364 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			364 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| 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\Attribute;
 | |
| 
 | |
| use Notes\Annotation;
 | |
| 
 | |
| use Notes\ObjectReflection;
 | |
| 
 | |
| class EntityResolver {
 | |
| 
 | |
|     const KEY_ENTITY_NAME = 01;
 | |
|     
 | |
|     const KEY_COLUMN_NAME = 02;
 | |
| 
 | |
|     const KEY_LC_ENTITY_NAME = 03;
 | |
| 
 | |
|     public string $entityClass;
 | |
| 
 | |
|     public array $uses;
 | |
| 
 | |
|     public array $class;
 | |
| 
 | |
|     public array $properties;
 | |
| 
 | |
|     public array $methods;
 | |
|     
 | |
|     protected array $fieldList = [];
 | |
| 
 | |
|     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, $cache)->read()
 | |
|         );
 | |
|         
 | |
|         $this->resolveAnnotations();
 | |
|     }
 | |
| 
 | |
|     public function field($name, $fieldKey = self::KEY_ENTITY_NAME, $throwException = true) : ? array
 | |
|     {
 | |
|         try{
 | |
|             return $this->fieldList($fieldKey)[$name] ?? null;
 | |
|         }
 | |
|         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 searchField($name) : null|array
 | |
|     {
 | |
|         return $this->field($name, self::KEY_ENTITY_NAME, false) ?: $this->field($name, self::KEY_COLUMN_NAME, false);
 | |
|     }
 | |
| 
 | |
|     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 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']);
 | |
|                             break;
 | |
| 
 | |
| 
 | |
|                         case static::KEY_ENTITY_NAME:
 | |
|                             $key = $item['name'];
 | |
|                         break;
 | |
| 
 | |
|                         case static::KEY_COLUMN_NAME:
 | |
|                             $key = strtolower($tag['object']->name ?? $item['name']);
 | |
|                         break;
 | |
| 
 | |
|                         default:
 | |
|                             throw new \InvalidArgumentException("Given `fieldKey` is unknown to the EntityResolver");
 | |
|                     }
 | |
| 
 | |
|                     $fieldList[$key] = $item;
 | |
|                     
 | |
|                     break;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $fieldList;
 | |
|     }
 | |
| 
 | |
|     public function relation(string $name) : ? array
 | |
|     {
 | |
|         try{
 | |
|             if ( null !== ( $this->properties[$name] ?? null ) ) {
 | |
|                 foreach($this->properties[$name]['tags'] ?? [] as $tag) {
 | |
|                     if ( $tag['object'] instanceof Relation or $tag['object'] instanceof Attribute\Property\Relation ) {
 | |
|                         return $this->properties[$name];
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             
 | |
|             return [];
 | |
|         }
 | |
|         catch(\Throwable $e) {
 | |
|            # if ( $throwException) {
 | |
|                 throw new \InvalidArgumentException("Can't find entity relation's column named `$name` from entity {$this->entityClass}");   
 | |
|            # }
 | |
|         }
 | |
|         
 | |
|         return null;
 | |
|     }
 | |
|     
 | |
|     public function searchFieldAnnotation(string $field, array|object|string $annotationType, bool $caseSensitive = true) : ? object
 | |
|     {
 | |
|         return $this->searchFieldAnnotationList($field, $annotationType, $caseSensitive)[0] ?? null;
 | |
|     }
 | |
|     
 | |
|     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) {
 | |
|                 foreach($annotations as $annotation) {
 | |
|                     if ( $tag['object'] instanceof $annotation ) {
 | |
|                         $list[] = $tag['object'];
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         return $list;
 | |
|     }
 | |
|     
 | |
|     public function tableName($required = false) : string
 | |
|     {
 | |
|         $table = $this->tableAnnotation($required);
 | |
| 
 | |
|         if ( ( $table->name ?? "" ) === "") {
 | |
|             if ($required) {
 | |
|                 throw new \ArgumentCountError("Your entity {$this->entityClass} seems to be missing a `name` argument for your @Table() annotation");
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $table->name ?? "";
 | |
|     }
 | |
| 
 | |
|     public function tableAnnotation($required = false) : null|Table|Attribute\Obj\Table
 | |
|     {
 | |
|         if ( null === $table = $this->getTableAttribute() ) {
 | |
|             if ($required) {
 | |
|                 throw new \LogicException("Your entity {$this->entityClass} seems to be missing a @Table() annotation");
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $table;
 | |
|     }
 | |
| 
 | |
|     public function databaseName() : ? string
 | |
|     {
 | |
|         return $this->tableAnnotation(false)->database ?? $this->databaseAdapter()->adapter()->databaseName() ?? null;
 | |
|     }
 | |
| 
 | |
|     public function sqlAdapter() : \Ulmus\ConnectionAdapter
 | |
|     {
 | |
|         if ( $adapterObj = $this->getAdapterInterfaceAttribute()) {
 | |
|             if ( false !== $adapterName = $adapterObj->adapter() ) {
 | |
|                 if ( null === ( $adapter = \Ulmus\Ulmus::$registeredAdapters[$adapterName] ?? null ) ) {
 | |
|                     throw new \Exception("Requested database adapter `$adapterName` is not registered.");
 | |
|                 }
 | |
|                 else {
 | |
|                     return $adapter;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return Ulmus::$defaultAdapter;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Alias of sqlAdapter
 | |
|      */
 | |
|     public function databaseAdapter() : \Ulmus\ConnectionAdapter
 | |
|     {
 | |
|         return $this->sqlAdapter();
 | |
|     }
 | |
| 
 | |
|     public function schemaName(bool $required = false) : ? string
 | |
|     {
 | |
|         if ( null === $table = $this->getTableAttribute() ) {
 | |
|             throw new \LogicException("Your entity {$this->entityClass} seems to be missing a @Table() annotation");
 | |
|         }
 | |
| 
 | |
|         if ( $required && ( ( $table->schema ?? "" ) === "" ) ) {
 | |
|             throw new \ArgumentCountError("Your entity {$this->entityClass} seems to be missing a `schema` argument for your @Table() annotation");
 | |
|         }
 | |
| 
 | |
|         return $table->schema ?? null;
 | |
|     }
 | |
| 
 | |
|     public function getPrimaryKeyField() : ? array
 | |
|     {
 | |
|         foreach($this->fieldList() as $key => $value) {
 | |
|             $field = $this->searchFieldAnnotation($key, [ Attribute\Property\Field::class, Field::class ]);
 | |
|             if ( null !== $field ) {
 | |
|                 if ( false !== ( $field->attributes['primary_key'] ?? false ) ) {
 | |
|                     return [ $key => $field ];
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         return null;
 | |
|     }
 | |
|     
 | |
|     public function getCompoundKeyFields() : ? array
 | |
|     {
 | |
|         return null;
 | |
|     }
 | |
|     
 | |
|     public function getUniqueFields() : ? array
 | |
|     {
 | |
|         return null;
 | |
|     }
 | |
| 
 | |
|     protected function getAdapterInterfaceAttribute() : null|object
 | |
|     {
 | |
|         return $this->getAttributeImplementing(Attribute\Obj\AdapterAttributeInterface::class);
 | |
|     }
 | |
| 
 | |
|     protected function getTableAttribute()
 | |
|     {
 | |
|         return $this->getAttributeImplementing(Attribute\Obj\Table::class) ?: $this->getAnnotationFromClassname( Table::class, false );
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 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);
 | |
|                 }
 | |
| 
 | |
|                 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->class['tags']) as $item) {
 | |
|             if ($item['object'] instanceof $interface) {
 | |
|                 return $item['object'];
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return null;
 | |
|     }
 | |
| 
 | |
|     public function instanciateAnnotationObject(array|\ReflectionAttribute $tagDefinition) : object
 | |
|     {
 | |
|         if ($tagDefinition instanceof \ReflectionAttribute) {
 | |
|             $obj = $tagDefinition->newInstance();
 | |
|         }
 | |
|         else {
 | |
|             $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)");
 | |
|             }
 | |
| 
 | |
|             $obj = new $class(... $arguments['constructor']);
 | |
| 
 | |
|             foreach ($arguments['setter'] as $key => $value) {
 | |
|                 $obj->$key = $value;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $obj;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Extracts arguments from an Annotation definition, easing object's declaration.
 | |
|      */
 | |
|     protected function extractArguments(array $arguments) : array
 | |
|     {
 | |
|         $list = [
 | |
|             'setter' => [],
 | |
|             'constructor' => [],
 | |
|         ];
 | |
| 
 | |
|         ksort($arguments);
 | |
| 
 | |
|         foreach($arguments as $key => $value) {
 | |
|             $list[ is_int($key) ? 'constructor' : 'setter' ][$key] = $value;
 | |
|         }
 | |
| 
 | |
|         return $list;
 | |
|     }
 | |
| 
 | |
|     protected function resolveAnnotations()
 | |
|     {
 | |
|         foreach($this->class['tags'] as &$tag) {
 | |
|             $tag['object'] ??= $this->instanciateAnnotationObject($tag);
 | |
|         }
 | |
| 
 | |
|         foreach($this->properties as &$property) {
 | |
|             foreach($property['tags'] as &$tag){
 | |
|                 $tag['object'] ??= $this->instanciateAnnotationObject($tag);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         foreach($this->methods as &$method) {
 | |
|             foreach($method['tags'] as &$tag){
 | |
|                 $tag['object'] ??= $this->instanciateAnnotationObject($tag);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 |