entity = $entity; $this->resolver = $entity::resolveEntity(); if ($repository) { $this->repository = $repository; } } public function searchRelation(string $name) : mixed { # Resolve relations here if one is called if ( $this->entity->isLoaded() ) { if ( false !== ( $dataset = $this->fetchFromDataset($name) ) ) { return $dataset; } if ( false !== $value = $this->resolveRelation($name) ) { return $value; } elseif ( false !== $value = $this->resolveVirtual($name) ) { return $value; } } else { if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class , Relation::class ] ) ) { return $this->instanciateEmptyObject($name, $relation); } elseif ($virtual = $this->resolveVirtual($name)) { return $virtual; } } return false; } protected function resolveVirtual(string $name) : mixed { 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 ]); } return call_user_func_array([ $this->entity, $virtual->method ], [ $this->entity ]); } return false; } protected function resolveRelation(string $name) : mixed { 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 { $this->oneToOne($name, $relation); } $this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository); 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() ]); 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() ]); if ($relation->bridgeField ?? false) { $collection = $relation->bridge::entityCollection(); foreach($results as $entity) { $collection->append($this->fetchFromDataset($relation->bridgeField, $entity->entityLoadedDataset)); } $this->entity->{$relation->bridgeField} = $collection; } return $results; } } return false; } protected function applyFilter(Repository $repository, string $name) : Repository { foreach($this->filters ?? [] as $filter) { $repository = call_user_func_array([ $this->entity, $filter->method ], [ $repository, $name, false ]); } return $repository; } protected function applyWhere() : void { if ($this->wheres) { $this->repository->open(); foreach($this->wheres as $condition) { $this->repository->where($condition->field, $condition->getValue($this->entity), $condition->operator); } $this->repository->close(); } } protected function applyOrderBy() : void { foreach($this->orders as $item) { $this->repository->orderBy($item->field, $item->order); } } protected function instanciateEmptyEntity(string $name, Relation|Attribute\Property\Relation $relation) : object { $class = $relation->entity ?? $this->resolver->properties[$name]['type']; return new $class(); } protected function instanciateEmptyObject(string $name, Relation|Attribute\Property\Relation $relation) : object { switch( true ) { case $relation->isOneToOne(): return $this->instanciateEmptyEntity($name, $relation); case $relation->isOneToMany(): return ($relation->entity ?? $this->resolver->properties[$name]['type'])::entityCollection(); case $relation->isManyToMany(): extract($this->relationAnnotations($name, $relation)); return $relation->bridgeField ?? false ? $relation->bridge::entityCollection() : $relationRelation->entity::entityCollection(); } throw new \InvalidArgumentException("Unknown or no relation was provided as relation type."); } protected function fetchFromDataset($name, ? array $data = null) : object|bool { $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; $isRelation = ( $annotation instanceof Relation ) || ( $annotation instanceof Attribute\Property\Relation ); if ( $isRelation && $annotation->isManyToMany() ) { $entity = $this->relationAnnotations($name, $annotation)['relationRelation']->entity; } else { $entity = $annotation->entity ?? $this->resolver->properties[$name]['type']; } $name = strtolower($name); foreach($data ?: $this->entity->entityLoadedDataset as $key => $value) { if ( $key === sprintf(static::SUBQUERY_FIELD_SUFFIX, strtolower($name)) ) { if ($value) { if ( null === ( $dataset = \json_decode($value, true) ) ) { throw new \Exception(sprintf("JSON error '%s' from '%s'", \json_last_error_msg(), $value)); } return $entity::entityCollection()->fromArray($dataset)->iterate(fn($e) => $e->resetVirtualProperties()); } else { return $entity::entityCollection(); } } elseif ( substr($key, 0, $len ) === sprintf(static::JOIN_FIELD_SEPARATOR, strtolower($name)) ) { $vars[substr($key, $len)] = $value; } else { # load here for API objects ! } } if ($vars) { if ( [] !== $data = (array_values(array_unique($vars)) !== [ null ] ? $vars : []) ) { return ( new $entity() )->fromArray($data)->resetVirtualProperties(); } else { return new $entity(); } } } return false; } public function oneToOne(string $name, Relation|Attribute\Property\Relation $relation) : Repository { return $this->oneToMany($name, $relation)->limit(1); } public function oneToMany(string $name, Relation|Attribute\Property\Relation $relation) : Repository { $baseEntity = $relation->entity ?? $this->resolver->properties[$name]['type']; $this->repository = $baseEntity::repository(); $this->applyWhere(); $this->applyOrderBy(); $field = $relation->key; if ($relation->foreignKey) { 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 ); } 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 { extract($this->relationAnnotations($name, $relation)); $this->repository = $relationRelation->entity::repository(); $bridgeAlias = $relation->bridgeField ?? uniqid("bridge_"); $relationAlias = $relation->field; $this->repository->select("{$this->repository->alias}.*") ->join(Query\Join::TYPE_INNER, $bridgeEntity->tableName(), $relation->bridge::field($relationRelation->key, $bridgeAlias), $relationRelation->entity::field($relationRelation->foreignKey), $bridgeAlias, function($join) { })->join(Query\Join::TYPE_INNER, $this->entity::resolveEntity()->tableName(), $relation->bridge::field($bridgeRelation->key, $bridgeAlias), $this->entity::field($bridgeRelation->foreignKey, $relationAlias), $relationAlias, function($join) { })->where( $this->entity::field($bridgeRelation->foreignKey, $relationAlias), is_string($this->entity) ? $this->entity::field($bridgeRelation->foreignKey) : $this->entity->{$bridgeRelation->foreignKey} ); $this->applyWhere(); $this->applyOrderBy(); if ($selectBridgeField && $relation->bridgeField) { $this->repository->selectEntity($relation->bridge, $bridgeAlias, $bridgeAlias); } return $this->applyFilter($this->repository, $name); } public static function relationAnnotations(string $name, Relation|Attribute\Property\Relation $relation) : array { if ( $relation->isOneToOne() || $relation->isManyToMany() ) { if ( ! $relation->hasBridge() ) { throw new \Exception("Your many-to-many @Relation() from variable `$name` is missing a 'bridge' field."); } $bridgeEntity = Ulmus::resolveEntity($relation->bridge); $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}"); } $relationRelation->entity ??= $relation->bridge::resolveEntity()->properties[$relation->foreignField]['type']; return [ 'bridgeEntity' => $bridgeEntity, 'bridgeRelation' => $bridgeRelation, 'relationRelation' => $relationRelation, ]; } return []; } }