- WIP on notes 2.x

This commit is contained in:
Dave M. 2024-05-28 08:59:12 -04:00
parent 2de3139c80
commit f3be11a590
42 changed files with 270 additions and 820 deletions

View File

@ -1,7 +0,0 @@
<?php
namespace Ulmus\Annotation\Classes;
class Collation implements \Notes\Annotation {
}

View File

@ -1,7 +0,0 @@
<?php
namespace Ulmus\Annotation\Classes;
class Method implements \Notes\Annotation {
}

View File

@ -1,27 +0,0 @@
<?php
namespace Ulmus\Annotation\Classes;
class Table implements \Notes\Annotation {
public string $name;
public string $database;
public string $schema;
public string $adapter;
public string $engine;
public function __construct($name = null, $engine = null)
{
if ( $name !== null ) {
$this->name = $name;
}
if ( $engine !== null ) {
$this->engine = $engine;
}
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class Field implements \Notes\Annotation {
public string $type;
public string $name;
public int|string $length;
public int $precision;
public array $attributes = [];
public bool $nullable;
public /* mixed */ $default;
public bool $readonly = false;
public function __construct(? string $type = null, ? int $length = null)
{
if ( $type !== null ) {
$this->type = $type;
}
if ( $length !== null ) {
$this->length = $length;
}
}
}

View File

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

View File

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

View File

@ -1,13 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class CreatedAt extends \Ulmus\Annotation\Property\Field {
public function __construct()
{
$this->nullable = false;
$this->type = "timestamp";
$this->attributes['default'] = "CURRENT_TIMESTAMP";
}
}

View File

@ -1,11 +0,0 @@
<?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

@ -1,11 +0,0 @@
<?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

@ -1,20 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
/**
* Since we need consistancy between the declaration of our ID and FK fields, it
* was decided to extend the Id class instead of Field for this case.
*/
class ForeignKey extends PrimaryKey {
public function __construct(? string $type = null, ? int $length = null)
{
parent::__construct($type, $length);
unset($this->nullable);
$this->attributes['primary_key'] = false;
$this->attributes['auto_increment'] = false;
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Id extends \Ulmus\Annotation\Property\Field\PrimaryKey {
public function __construct()
{
$this->attributes['unsigned'] = true;
$this->attributes['auto_increment'] = true;
parent::__construct('bigint');
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class PrimaryKey extends \Ulmus\Annotation\Property\Field {
public function __construct(? string $type = null, ? int $length = null)
{
$this->nullable = false;
$this->attributes['primary_key'] = true;
parent::__construct($type, $length);
}
}

View File

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

View File

@ -1,11 +0,0 @@
<?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

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

View File

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

View File

@ -1,14 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class UpdatedAt extends \Ulmus\Annotation\Property\Field {
public function __construct()
{
$this->nullable = true;
$this->type = "timestamp";
$this->attributes['update'] = "CURRENT_TIMESTAMP";
$this->attributes['default'] = null;
}
}

View File

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

View File

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

View File

@ -1,15 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class GroupBy implements \Notes\Annotation {
public array $fields = [];
public function __construct(...$field)
{
if ( $field ) {
$this->fields = $field;
}
}
}

View File

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

View File

@ -1,31 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class Join implements \Notes\Annotation {
public string $type;
public string|Stringable $key;
public string|Stringable $foreignKey;
public string $entity;
public string $alias;
public function __construct(? string $type = null, /*? string|Stringable*/ $key = null, /*? string|Stringable*/ $foreignKey = null)
{
if ($type !== null) {
$this->type = $type;
}
if ($key !== null) {
$this->key = $key;
}
if ($foreignKey !== null) {
$this->foreignKey = $foreignKey;
}
}
}

View File

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

View File

@ -1,14 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
use Ulmus\Query;
class OrWhere extends Where {
public function __construct(/* ? Stringable */ $field = null, $value = null, ? string $operator = null, ? string $condition = null)
{
parent::__construct($field, $value, $operator, Query\Where::CONDITION_OR);
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class OrderBy implements \Notes\Annotation {
public string $field;
public string $order = "ASC";
public function __construct(string $field = null, string $order = null)
{
if ( $field !== null ) {
$this->field = $field;
}
if ( $order !== null ) {
$this->order = $order;
}
}
}

View File

@ -1,88 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class Relation implements \Notes\Annotation {
public string $type;
public /*stringable*/ $key;
public /* callable */ $generateKey;
public /*stringable*/ $foreignKey;
public array $foreignKeys;
public string $bridge;
public /*stringable*/ $bridgeKey;
public /*stringable*/ $bridgeForeignKey;
public string $entity;
public string $join;
public string $function = "loadAll";
public function __construct(string $type = null)
{
if ( $type !== null ) {
$this->type = $type;
}
}
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 isset($this->bridge);
}
}

View File

@ -1,5 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Relation;
class Ignore implements \Notes\Annotation {}

View File

@ -1,19 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class Virtual extends Field {
public bool $readonly = true;
public \Closure $closure;
public string $method;
public function __construct(? \Closure $closure = null)
{
if ( $closure !== null ) {
$this->closure = $closure;
}
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
use Ulmus\Query;
class Where implements \Notes\Annotation {
public /* stringable */ $field;
public $value;
public string $operator;
public string $condition;
public function __construct(/* ? Stringable */ $field = null, $value = null, ? string $operator = null, ? string $condition = null)
{
if ( $field !== null ) {
$this->field = $field;
}
if ( $value !== null ) {
$this->value = $value;
}
$this->operator = $operator !== null ? $operator : Query\Where::OPERATOR_EQUAL;
$this->condition = $condition !== null ? $condition : Query\Where::CONDITION_AND;
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class WithJoin implements \Notes\Annotation {
protected array $joins;
public function __construct(/*Stringable|array|null*/ $joins = null)
{
if ( $joins ) {
$this->joins = (array)$joins;
}
}
}

View File

@ -5,7 +5,7 @@ namespace Ulmus\Attribute\Property;
use Ulmus\Attribute\Attribute; use Ulmus\Attribute\Attribute;
#[\Attribute] #[\Attribute]
class Join { class Join implements ResettablePropertyInterface {
public function __construct( public function __construct(
public string $type, public string $type,
public null|string|\Stringable|array $key = null, public null|string|\Stringable|array $key = null,

View File

@ -5,7 +5,7 @@ namespace Ulmus\Attribute\Property;
use Ulmus\Attribute\Attribute; use Ulmus\Attribute\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY)] #[\Attribute(\Attribute::TARGET_PROPERTY)]
class Relation { class Relation implements ResettablePropertyInterface {
public function __construct( public function __construct(
public Relation\RelationTypeEnum|string $type, public Relation\RelationTypeEnum|string $type,
public \Stringable|string|array $key = "", public \Stringable|string|array $key = "",

View File

@ -0,0 +1,8 @@
<?php
namespace Ulmus\Attribute\Property;
interface ResettablePropertyInterface
{
}

View File

@ -3,7 +3,7 @@
namespace Ulmus\Attribute\Property; namespace Ulmus\Attribute\Property;
#[\Attribute(\Attribute::TARGET_PROPERTY)] #[\Attribute(\Attribute::TARGET_PROPERTY)]
class Virtual extends Field { class Virtual extends Field implements ResettablePropertyInterface {
public bool $readonly = true; public bool $readonly = true;

View File

@ -6,13 +6,11 @@ use Notes\Common\ReflectedClass;
use Notes\Common\ReflectedProperty; use Notes\Common\ReflectedProperty;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use Ulmus\Ulmus, use Ulmus\Ulmus,
Ulmus\Annotation\Classes\Table, Ulmus\Attribute\Obj\Table,
Ulmus\Annotation\Property\Field, Ulmus\Attribute\Obj\AdapterAttributeInterface,
Ulmus\Annotation\Property\Virtual, Ulmus\Attribute\Property\Field,
Ulmus\Annotation\Property\Relation, Ulmus\Attribute\Property\Relation,
Ulmus\Attribute; Ulmus\Attribute\Property\Virtual;
use Notes\Annotation;
use Notes\ObjectReflection; use Notes\ObjectReflection;
@ -37,7 +35,7 @@ class EntityResolver {
$this->reflectedClass = ObjectReflection::fromClass($entityClass, $cache)->reflectClass(); $this->reflectedClass = ObjectReflection::fromClass($entityClass, $cache)->reflectClass();
} }
public function field($name, $fieldKey = self::KEY_ENTITY_NAME, $throwException = true) : ? ReflectedProperty public function field($name, $fieldKey = self::KEY_ENTITY_NAME, $throwException = true) : null|ReflectedProperty
{ {
try{ try{
return $this->fieldList($fieldKey)[$name] ?? null; return $this->fieldList($fieldKey)[$name] ?? null;
@ -51,7 +49,7 @@ class EntityResolver {
return null; return null;
} }
public function searchField($name) : null|array public function searchField($name) : null|ReflectedProperty
{ {
return $this->field($name, self::KEY_ENTITY_NAME, false) ?: $this->field($name, self::KEY_COLUMN_NAME, false); return $this->field($name, self::KEY_ENTITY_NAME, false) ?: $this->field($name, self::KEY_COLUMN_NAME, false);
} }
@ -62,8 +60,8 @@ class EntityResolver {
foreach($this->reflectedClass->getProperties(true) as $item) { foreach($this->reflectedClass->getProperties(true) as $item) {
foreach($item->getAttributes() as $tag) { foreach($item->getAttributes() as $tag) {
if ( $tag->object instanceof Attribute\Property\Field ) { if ( $tag->object instanceof Field ) {
if ( $skipVirtual && $tag->object instanceof Attribute\Property\Virtual ) { if ( $skipVirtual && $tag->object instanceof Virtual ) {
break; break;
} }
@ -102,7 +100,7 @@ class EntityResolver {
try{ try{
if ( $property ) { if ( $property ) {
foreach($property->getAttributes() as $tag) { foreach($property->getAttributes() as $tag) {
if ( $tag->object instanceof Relation or $tag->object instanceof Attribute\Property\Relation ) { if ( $tag->object instanceof Relation ) {
return $property; return $property;
} }
} }
@ -156,7 +154,7 @@ class EntityResolver {
return $table->name ?? ""; return $table->name ?? "";
} }
public function tableAnnotation($required = false) : null|Table|Attribute\Obj\Table public function tableAnnotation($required = false) : null|Table
{ {
if ( null === $table = $this->getTableAttribute() ) { if ( null === $table = $this->getTableAttribute() ) {
if ($required) { if ($required) {
@ -167,7 +165,7 @@ class EntityResolver {
return $table; return $table;
} }
public function databaseName() : ? string public function databaseName() : null|string
{ {
return $this->tableAnnotation(false)->database ?? $this->databaseAdapter()->adapter()->databaseName() ?? null; return $this->tableAnnotation(false)->database ?? $this->databaseAdapter()->adapter()->databaseName() ?? null;
} }
@ -196,7 +194,7 @@ class EntityResolver {
return $this->sqlAdapter(); return $this->sqlAdapter();
} }
public function schemaName(bool $required = false) : ? string public function schemaName(bool $required = false) : null|string
{ {
if ( null === $table = $this->getTableAttribute() ) { if ( null === $table = $this->getTableAttribute() ) {
throw new \LogicException("Your entity {$this->entityClass} seems to be missing a @Table() annotation"); throw new \LogicException("Your entity {$this->entityClass} seems to be missing a @Table() annotation");
@ -209,10 +207,10 @@ class EntityResolver {
return $table->schema ?? null; return $table->schema ?? null;
} }
public function getPrimaryKeyField() : ? array public function getPrimaryKeyField() : null|array
{ {
foreach($this->fieldList() as $key => $value) { foreach($this->fieldList() as $key => $value) {
$field = $this->searchFieldAnnotation($key, [ Attribute\Property\Field::class, Field::class ]); $field = $this->searchFieldAnnotation($key, [ Field::class ]);
if ( null !== $field ) { if ( null !== $field ) {
if ( false !== ( $field->attributes['primary_key'] ?? false ) ) { if ( false !== ( $field->attributes['primary_key'] ?? false ) ) {
return [ $key => $field ]; return [ $key => $field ];
@ -235,55 +233,15 @@ class EntityResolver {
protected function getAdapterInterfaceAttribute() : null|object protected function getAdapterInterfaceAttribute() : null|object
{ {
return $this->getAttributeImplementing(Attribute\Obj\AdapterAttributeInterface::class); return $this->getAttributeImplementing(AdapterAttributeInterface::class);
} }
protected function getTableAttribute() protected function getTableAttribute()
{ {
return $this->getAttributeImplementing(Attribute\Obj\Table::class) ?: $this->getAnnotationFromClassname( Table::class, false ); return $this->getAttributeImplementing(Table::class);
} }
/** public function getAttributeImplementing(string $interface) : null|object
* Transform an annotation into it's object's counterpart
*/
public function getAnnotationFromClassname(string $className, bool $throwError = true) : ? object
{
exit(__FILE__);
if ( $name = $this->uses[$className] ?? false ) {
foreach(array_reverse($this->class['tags']) as $item) {
if ( $item['tag'] === $name ) {
return $this->instanciateAnnotationObject($item);
}
foreach($this->properties as $item) {
foreach(array_reverse($item['tags']) as $item) {
if ( $item['tag'] === $name ) {
return $this->instanciateAnnotationObject($item);
}
}
}
foreach($this->methods as $item) {
foreach(array_reverse($item['tags']) as $item) {
if ( $item['tag'] === $name ) {
return $this->instanciateAnnotationObject($item);
}
}
}
}
if ($throwError) {
throw new \TypeError("Annotation `$className` could not be found within your object `{$this->entityClass}`");
}
}
elseif ($throwError) {
throw new \InvalidArgumentException("Class `$className` was not found within {$this->entityClass} uses statement (or it's children / traits)");
}
return null;
}
public function getAttributeImplementing(string $interface) : ? object
{ {
foreach (array_reverse($this->reflectedClass->getAttributes(true)) as $item) { foreach (array_reverse($this->reflectedClass->getAttributes(true)) as $item) {
if ($item->object instanceof $interface) { if ($item->object instanceof $interface) {
@ -293,14 +251,4 @@ class EntityResolver {
return null; return null;
} }
public function instanciateAnnotationObject(array|\ReflectionAttribute $tagDefinition) : object
{
if ($tagDefinition instanceof \ReflectionAttribute) {
$obj = $tagDefinition->newInstance();
}
return $obj;
}
} }

View File

@ -0,0 +1,153 @@
<?php
namespace Ulmus\Entity;
use Generator;
use Ulmus\Attribute\Property\Field;
use Ulmus\Attribute\Property\Relation;
use Ulmus\Common\EntityField;
use Ulmus\Common\EntityResolver;
use Ulmus\Ulmus;
class DatasetHandler
{
public function __construct(
protected EntityResolver $entityResolver,
protected bool $entityStrictFieldsDeclaration = false,
) {}
public function pull(object $entity) : Generator
{
foreach($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true) as $key => $field) {
$annotation = $this->entityResolver->searchFieldAnnotation($key,[ Field::class ]);
if ( $entity->__isset($key) ) {
yield $annotation->name ?? $key => $entity->$key;
}
elseif ( $field->allowsNull() ) {
yield $annotation->name ?? $key => null;
}
}
}
public function push(iterable $dataset) : Generator|array
{
$unmatched = [];
foreach($dataset as $key => $value) {
$field = $this->entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? $this->entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false);
if ( $field === null ) {
if ($this->entityStrictFieldsDeclaration ) {
throw new \Exception("Field `$key` can not be found within your entity ".static::class);
}
else {
$unmatched[$key] = $value;
}
continue;
}
$type = $field->getTypes()[0];
if ( is_null($value) ) {
yield $field->name => null;
}
elseif ( $field->expectType('array') ) {
if ( is_string($value)) {
if (substr($value, 0, 1) === "a") {
yield $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));
}
yield $field->name => $data;
}
}
elseif ( is_array($value) ) {
yield $field->name => $value;
}
}
elseif ( EntityField::isScalarType($type->type) ) {
if ( $type->type === 'string' ) {
$annotation = $this->entityResolver->searchFieldAnnotation($field->name, [ Field::class ] );
if ( $annotation->length ?? null ) {
$value = mb_substr($value, 0, $annotation->length);
}
}
elseif ( $type->type === 'bool' ) {
$value = (bool) $value;
}
yield $field->name => $value;
}
elseif ( $value instanceof \UnitEnum ) {
yield $field->name => $value;
}
elseif (enum_exists($type->type)) {
yield $field->name => $type->type::from($value);
}
elseif ( ! $type->builtIn ) {
try {
yield $field->name => Ulmus::instanciateObject($type->type, [ $value ]);
}
catch(\Error $e) {
throw new \Error(sprintf("%s for class '%s' on field '%s'", $e->getMessage(), get_class($this), $field->name));
}
}
}
return $unmatched;
}
public function pullRelation(object $entity) : Generator
{
foreach($this->entityResolver->reflectedClass->getProperties(true) as $name => $field){
$relation = $this->entityResolver->searchFieldAnnotation($name, [ Relation::class ] );
if ($relation) {
$ignore = $this->entityResolver->searchFieldAnnotation($name, [ Relation\Ignore::class ] );
if ($ignore && $ignore->ignoreExport) {
if ( $relation->isOneToOne() ) {
# @TODO TO INCLUDED INTO getTypes() RETURNED CLASS WHEN DONE !
yield $name => ( new \ReflectionClass($field->getTypes()[0]) )->newInstanceWithoutConstructor();
}
else {
# empty collection
yield $name => [];
}
continue;
}
# @TODO Must fix recursive bug.. this last check is way too basic to work
if ( $entity->__isset($name) && ($relation->entity ?? $relation->bridge) !== static::class ) {
if ( null !== $value = $entity->__isset($name) ?? null ) {
if ( is_iterable($value) ) {
$list = [];
foreach($value as $entityObj) {
$list[] = $entityObj->entityGetDataset(false);
}
yield $name => $list;
}
elseif ( is_object($value) ) {
yield $name => $value->entityGetDataset(false);
}
}
}
}
}
}
}

View File

@ -4,7 +4,15 @@ namespace Ulmus;
use Notes\Attribute\Ignore; use Notes\Attribute\Ignore;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Ulmus\{Common\EntityResolver, Common\EntityField, Entity\EntityInterface, QueryBuilder\QueryBuilderInterface}; use Ulmus\{Attribute\Property\Join,
Attribute\Property\Relation,
Attribute\Property\ResettablePropertyInterface,
Attribute\Property\Virtual,
Common\EntityResolver,
Common\EntityField,
Entity\DatasetHandler,
Entity\EntityInterface,
QueryBuilder\QueryBuilderInterface};
use Ulmus\SearchRequest\{Attribute\SearchParameter, use Ulmus\SearchRequest\{Attribute\SearchParameter,
SearchMethodEnum, SearchMethodEnum,
SearchRequestInterface, SearchRequestInterface,
@ -14,6 +22,9 @@ use Ulmus\SearchRequest\{Attribute\SearchParameter,
trait EntityTrait { trait EntityTrait {
use EventTrait; use EventTrait;
#[Ignore]
public array $entityLoadedDataset = [];
#[Ignore] #[Ignore]
protected bool $entityStrictFieldsDeclaration = false; protected bool $entityStrictFieldsDeclaration = false;
@ -21,14 +32,23 @@ trait EntityTrait {
protected array $entityDatasetUnmatchedFields = []; protected array $entityDatasetUnmatchedFields = [];
#[Ignore] #[Ignore]
public array $entityLoadedDataset = []; protected DatasetHandler $datasetHandler;
#[Ignore] #[Ignore]
public function __construct(array $dataset = null) { public function __construct(iterable|null $dataset = null)
{
$this->initializeEntity($dataset);
}
#[Ignore]
public function initializeEntity(iterable|null $dataset = null) : void
{
if ($dataset) { if ($dataset) {
$this->entityFillFromDataset($dataset); $this->fromArray($dataset);
} }
$this->datasetHandler = new DatasetHandler(static::resolveEntity(), $this->entityStrictFieldsDeclaration);
$this->resetVirtualProperties(); $this->resetVirtualProperties();
} }
@ -37,98 +57,55 @@ trait EntityTrait {
{ {
$loaded = $this->isLoaded(); $loaded = $this->isLoaded();
$entityResolver = $this->resolveEntity(); $handler = $this->datasetHandler->push($dataset);
foreach($dataset as $key => $value) { foreach($handler as $field => $value) {
$field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? $entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false); $this->$field = $value;
}
# Temp. fix, incoming patch soon $this->entityDatasetUnmatchedFields = $handler->getReturn();
$type = $field->getTypes()[0];
if ( $field === null ) { # Keeping original data to diff on UPDATE query
if ($this->entityStrictFieldsDeclaration ) { if ( ! $loaded ) {
throw new \Exception("Field `$key` can not be found within your entity ".static::class); $this->entityLoadedDataset = array_change_key_case(is_array($dataset) ? $dataset : iterator_to_array($dataset), \CASE_LOWER);
} }
else { elseif ($overwriteDataset) {
$this->entityDatasetUnmatchedFields[$key] = $value; $this->entityLoadedDataset = array_change_key_case(is_array($dataset) ? $dataset : iterator_to_array($dataset), \CASE_LOWER) + $this->entityLoadedDataset;
}
}
elseif ( is_null($value) ) {
$this->{$field->name} = null;
}
elseif ( $field->expectType('array') ) {
if ( is_string($value)) {
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;
}
}
elseif ( EntityField::isScalarType($type->type) ) {
if ( $type->type === 'string' ) {
$annotation = $entityResolver->searchFieldAnnotation($field->name, [ Attribute\Property\Field::class ] );
if ( $annotation->length ?? null ) {
$value = mb_substr($value, 0, $annotation->length);
}
}
elseif ( $type->type === 'bool' ) {
$value = (bool) $value;
}
$this->{$field->name} = $value;
}
elseif ( $value instanceof \UnitEnum ) {
$this->{$field->name} = $value;
}
elseif (enum_exists($type->type)) {
$this->{$field->name} = $type->type::from($value);
}
elseif ( ! $type->builtIn ) {
try {
$this->{$field->name} = Ulmus::instanciateObject($type->type, [ $value ]);
}
catch(\Error $e) {
$f = $type->type;
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
if ( ! $loaded /* || $isLoadedDataset */ ) {
#if ( $field !== null ) {
# $annotation = $entityResolver->searchFieldAnnotation($field['name'], new Field() );
# $this->entityLoadedDataset[$annotation ? $annotation->name : $field['name']] = $dataset; # <--------- THIS TO FIX !!!!!!
#}
$this->entityLoadedDataset = array_change_key_case($dataset, \CASE_LOWER);
}
elseif ($overwriteDataset) {
$this->entityLoadedDataset = iterator_to_array(array_change_key_case($dataset, \CASE_LOWER)) + $this->entityLoadedDataset;
}
} }
return $this; return $this;
} }
#[Ignore]
public function entityGetDataset(bool $includeRelations = false, bool $returnSource = false, bool $rewriteValue = true) : array
{
if ( $returnSource ) {
return $this->entityLoadedDataset;
}
$dataset = [];
foreach($this->datasetHandler->pull($this) as $field => $value) {
$dataset[$field] = $rewriteValue ? static::repository()->adapter->adapter()->writableValue($value) : $value;
}
if ($includeRelations) {
foreach($this->datasetHandler->pullRelation($this) as $field => $object) {
$dataset[$field] = $object;
}
}
return $dataset;
}
#[Ignore] #[Ignore]
public function resetVirtualProperties() : self public function resetVirtualProperties() : self
{ {
foreach($this->resolveEntity()->reflectedClass->getProperties(true) as $prop => $property) { foreach($this->resolveEntity()->reflectedClass->getProperties(true) as $field => $property) {
foreach($property->attributes as $tag) { foreach($property->attributes as $tag) {
if ( in_array(strtolower($tag->tag), [ 'relation', 'join', 'virtual' ] ) ) {
unset($this->$prop); if ( $tag->object instanceof ResettablePropertyInterface ) {
unset($this->$field);
} }
} }
} }
@ -146,74 +123,6 @@ trait EntityTrait {
return $this->entityFillFromDataset($dataset); return $this->entityFillFromDataset($dataset);
} }
public function entityGetDataset(bool $includeRelations = false, bool $returnSource = false, bool $rewriteValue = true) : array
{
if ( $returnSource ) {
return $this->entityLoadedDataset;
}
$dataset = [];
$entityResolver = $this->resolveEntity();
foreach($entityResolver->fieldList(Common\EntityResolver::KEY_ENTITY_NAME, true) as $key => $field) {
$annotation = $entityResolver->searchFieldAnnotation($key, [ Attribute\Property\Field::class ]);
if ( isset($this->$key) ) {
$dataset[$annotation->name ?? $key] = $rewriteValue ?
static::repository()->adapter->adapter()->writableValue($this->$key)
:
$this->$key;
}
elseif ( $field->allowsNull() ) {
$dataset[$annotation->name ?? $key] = null;
}
}
if ($includeRelations) {
foreach($entityResolver->reflectedClass->getProperties(true) as $name => $field){
$relation = $entityResolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class ] );
if ($relation) {
$ignore = $entityResolver->searchFieldAnnotation($name, [ Attribute\Property\Relation\Ignore::class ] );
if ($ignore && $ignore->ignoreExport) {
if ( $relation->isOneToOne() ) {
# empty object
$dataset[$name] = ( new \ReflectionClass($field->getTypes()[0]) )->newInstanceWithoutConstructor();
}
else {
# empty collection
$dataset[$name] = [];
}
continue;
}
# @TODO Must fix recursive bug.. this last check is way too basic to work
if ( isset($this->$name) && ($relation->entity ?? $relation->bridge) !== static::class ) {
if ( null !== $value = $this->$name ?? null ) {
if ( is_iterable($value) ) {
$list = [];
foreach($value as $entity) {
$list[] = $entity->entityGetDataset(false);
}
$dataset[$name] = $list;
}
elseif ( is_object($value) ) {
$dataset[$name] = $value->entityGetDataset(false);
}
}
}
}
}
}
return $dataset;
}
#[Ignore] #[Ignore]
public function toArray($includeRelations = false, array $filterFields = null, bool $rewriteValue = true) : array public function toArray($includeRelations = false, array $filterFields = null, bool $rewriteValue = true) : array
{ {

View File

@ -2,20 +2,13 @@
namespace Ulmus\Repository; namespace Ulmus\Repository;
use Ulmus\{Attribute\Property\Field, use Ulmus\{
Ulmus, Ulmus, Query, Common\EntityResolver, Repository, Event,
Annotation, };
Attribute,
Query,
Common,
Common\EntityResolver,
Repository,
Event,
EntityCollection};
use Ulmus\Annotation\Property\{Filter, OrderBy, Relation, Relation\Ignore as RelationIgnore, Where, WithJoin, }; use Ulmus\Attribute\Property\{
Filter, Join, OrderBy, Relation, Virtual, Where, WithJoin,
use Closure; };
class RelationBuilder class RelationBuilder
{ {
@ -64,7 +57,7 @@ class RelationBuilder
} }
else { else {
if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class , Relation::class ] ) ) { if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class ] ) ) {
return $this->instanciateEmptyObject($name, $relation); return $this->instanciateEmptyObject($name, $relation);
} }
elseif ($virtual = $this->resolveVirtual($name)) { elseif ($virtual = $this->resolveVirtual($name)) {
@ -77,7 +70,7 @@ class RelationBuilder
protected function resolveVirtual(string $name) : mixed protected function resolveVirtual(string $name) : mixed
{ {
if (null !== ($virtual = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Virtual::class, Annotation\Property\Virtual::class ]))) { if (null !== ($virtual = $this->resolver->searchFieldAnnotation($name, Virtual::class))) {
try { try {
$arguments = [ $this->entity ]; $arguments = [ $this->entity ];
@ -98,11 +91,11 @@ class RelationBuilder
protected function resolveRelation(string $name) : mixed protected function resolveRelation(string $name) : mixed
{ {
if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class, Relation::class ] ) ) ) { if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class ] ) ) ) {
$this->orders = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\OrderBy::class, OrderBy::class ] ); $this->orders = $this->resolver->searchFieldAnnotationList($name, [ OrderBy::class ] );
$this->wheres = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\Where::class, Where::class ] ); $this->wheres = $this->resolver->searchFieldAnnotationList($name, [ Where::class ] );
$this->filters = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\Filter::class, Filter::class ] ); $this->filters = $this->resolver->searchFieldAnnotationList($name, [ Filter::class ] );
$this->joins = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\WithJoin::class, WithJoin::class ] ); $this->joins = $this->resolver->searchFieldAnnotationList($name, [ WithJoin::class ] );
switch( true ) { switch( true ) {
case $relation->isOneToOne(): case $relation->isOneToOne():
@ -178,22 +171,22 @@ class RelationBuilder
} }
} }
protected function instanciateEmptyEntity(string $name, Relation|Attribute\Property\Relation $relation) : object protected function instanciateEmptyEntity(string $name, Relation $relation) : object
{ {
$class = $relation->entity ?? $this->resolver->properties[$name]['type']; $class = $relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type;
return new $class(); return new $class();
} }
protected function instanciateEmptyObject(string $name, Relation|Attribute\Property\Relation $relation) : object protected function instanciateEmptyObject(string $name, Relation $relation) : object
{ {
switch( true ) { switch( true ) {
case $relation->isOneToOne(): case $relation->isOneToOne():
return $this->instanciateEmptyEntity($name, $relation); return $this->instanciateEmptyEntity($name, $relation);
case $relation->isOneToMany(): case $relation->isOneToMany():
return ($relation->entity ?? $this->resolver->properties[$name]['type'])::entityCollection(); return ($relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type)::entityCollection();
case $relation->isManyToMany(): case $relation->isManyToMany():
extract($this->relationAnnotations($name, $relation)); extract($this->relationAnnotations($name, $relation));
@ -206,14 +199,14 @@ class RelationBuilder
protected function fetchFromDataset($name, ? array $data = null) : object|bool protected function fetchFromDataset($name, ? array $data = null) : object|bool
{ {
$annotation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Join::class, Annotation\Property\Join::class ]) ?: $annotation = $this->resolver->searchFieldAnnotation($name, [ Join::class ]) ?:
$this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class, Annotation\Property\Relation::class ]); $this->resolver->searchFieldAnnotation($name, [ Relation::class ]);
if ( $annotation ) { if ( $annotation ) {
$vars = []; $vars = [];
$len = strlen( $name ) + 1; $len = strlen( $name ) + 1;
$isRelation = ( $annotation instanceof Relation ) || ( $annotation instanceof Attribute\Property\Relation ); $isRelation = $annotation instanceof Relation;
if ( $isRelation && $annotation->isManyToMany() ) { if ( $isRelation && $annotation->isManyToMany() ) {
$entity = $this->relationAnnotations($name, $annotation)['relationRelation']->entity; $entity = $this->relationAnnotations($name, $annotation)['relationRelation']->entity;
@ -259,12 +252,12 @@ class RelationBuilder
return false; return false;
} }
public function oneToOne(string $name, Relation|Attribute\Property\Relation $relation) : Repository public function oneToOne(string $name, Relation $relation) : Repository
{ {
return $this->oneToMany($name, $relation)->limit(1); return $this->oneToMany($name, $relation)->limit(1);
} }
public function oneToMany(string $name, Relation|Attribute\Property\Relation $relation) : Repository public function oneToMany(string $name, Relation $relation) : Repository
{ {
$baseEntity = $relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type; $baseEntity = $relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type;
@ -290,7 +283,7 @@ class RelationBuilder
return $this->applyFilter($this->repository, $name); return $this->applyFilter($this->repository, $name);
} }
public function manyToMany(string $name, Relation|Attribute\Property\Relation $relation, Relation|Attribute\Property\Relation & $relationRelation = null, bool $selectBridgeField = true) : Repository public function manyToMany(string $name, Relation $relation, Relation & $relationRelation = null, bool $selectBridgeField = true) : Repository
{ {
extract($this->relationAnnotations($name, $relation)); extract($this->relationAnnotations($name, $relation));
@ -318,7 +311,7 @@ class RelationBuilder
return $this->applyFilter($this->repository, $name); return $this->applyFilter($this->repository, $name);
} }
public static function relationAnnotations(string $name, Relation|Attribute\Property\Relation $relation) : array public static function relationAnnotations(string $name, Relation $relation) : array
{ {
if ( $relation->isOneToOne() || $relation->isManyToMany() ) { if ( $relation->isOneToOne() || $relation->isManyToMany() ) {
if ( ! $relation->hasBridge() ) { if ( ! $relation->hasBridge() ) {
@ -326,14 +319,14 @@ class RelationBuilder
} }
$bridgeEntity = Ulmus::resolveEntity($relation->bridge); $bridgeEntity = Ulmus::resolveEntity($relation->bridge);
$bridgeRelation = $bridgeEntity->searchFieldAnnotation($relation->field, [ Attribute\Property\Relation::class, Relation::class ]); $bridgeRelation = $bridgeEntity->searchFieldAnnotation($relation->field, [ Relation::class ]);
$relationRelation = $bridgeEntity->searchFieldAnnotation($relation->foreignField, [ Attribute\Property\Relation::class, Relation::class ]); $relationRelation = $bridgeEntity->searchFieldAnnotation($relation->foreignField, [ Relation::class ]);
if ($relationRelation === null) { if ($relationRelation === null) {
throw new \Exception("@Relation annotation not found for field `{$relation->foreignField}` in entity {$relation->bridge}"); throw new \Exception("@Relation annotation not found for field `{$relation->foreignField}` in entity {$relation->bridge}");
} }
$relationRelation->entity ??= $relation->bridge::resolveEntity()->properties[$relation->foreignField]['type']; $relationRelation->entity ??= $relation->bridge::resolveEntity()->reflectedClass->getProperties()[$relation->foreignField]->getTypes()[0]->type;
return [ return [
'bridgeEntity' => $bridgeEntity, 'bridgeEntity' => $bridgeEntity,