├── src ├── Events │ ├── Created.php │ ├── Creating.php │ ├── Deleted.php │ ├── Deleting.php │ ├── Stored.php │ ├── Storing.php │ ├── Updated.php │ ├── Updating.php │ ├── Initializing.php │ ├── Event.php │ └── Initialized.php ├── Exceptions │ ├── CacheException.php │ ├── MappingException.php │ ├── EntityMapNotFoundException.php │ └── EntityNotFoundException.php ├── Relationships │ ├── PivotMap.php │ ├── HasMany.php │ ├── MorphMany.php │ ├── MorphOne.php │ ├── HasOne.php │ ├── MorphPivot.php │ ├── MorphOneOrMany.php │ ├── EmbedsMany.php │ ├── Pivot.php │ ├── MorphToMany.php │ ├── EmbedsOne.php │ ├── MorphTo.php │ ├── EmbeddedRelationship.php │ ├── HasOneOrMany.php │ ├── HasManyThrough.php │ ├── BelongsTo.php │ └── Relationship.php ├── Notifications │ ├── Notification.php │ └── NotificationMap.php ├── Plugins │ ├── SoftDeletes │ │ ├── Events │ │ │ ├── Restored.php │ │ │ └── Restoring.php │ │ ├── Restore.php │ │ ├── SoftDeletingScope.php │ │ └── SoftDeletesPlugin.php │ ├── AnaloguePluginInterface.php │ ├── AnaloguePlugin.php │ └── Timestamps │ │ └── TimestampsPlugin.php ├── Drivers │ ├── IlluminateQueryBuilder.php │ ├── DriverInterface.php │ ├── IlluminateConnectionProvider.php │ ├── CapsuleConnectionProvider.php │ ├── Manager.php │ ├── IlluminateDriver.php │ ├── DBAdapter.php │ └── IlluminateDBAdapter.php ├── AnalogueFacade.php ├── System │ ├── Builders │ │ ├── ResultBuilderInterface.php │ │ ├── ResultBuilderFactory.php │ │ ├── PolymorphicResultBuilder.php │ │ ├── EntityBuilder.php │ │ └── ResultBuilder.php │ ├── ScopeInterface.php │ ├── Wrappers │ │ └── Factory.php │ ├── InternallyMappable.php │ ├── Cache │ │ ├── CachedRelationship.php │ │ ├── InstanceCache.php │ │ └── AttributeCache.php │ ├── Proxies │ │ └── ProxyFactory.php │ ├── MapperFactory.php │ └── SingleTableInheritanceScope.php ├── ValueObject.php ├── Mappable.php ├── MagicSetters.php ├── LengthAwareEntityPaginator.php ├── helpers.php ├── Commands │ ├── Command.php │ ├── Delete.php │ └── Store.php ├── MagicGetters.php ├── MappableTrait.php ├── AnalogueServiceProvider.php ├── MagicCasting.php ├── Analogue.php ├── Entity.php ├── Repository.php ├── ValueMap.php └── EntityCollection.php ├── LICENSE.txt └── composer.json /src/Events/Created.php: -------------------------------------------------------------------------------- 1 | entity = $entity; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Plugins/AnaloguePluginInterface.php: -------------------------------------------------------------------------------- 1 | mapper = $mapper; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/System/Builders/ResultBuilderInterface.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/System/ScopeInterface.php: -------------------------------------------------------------------------------- 1 | getEntityMap()->getInheritanceType()) { 12 | case 'single_table': 13 | return new PolymorphicResultBuilder($mapper); 14 | default: 15 | return new ResultBuilder($mapper, !$skipCache); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/MagicSetters.php: -------------------------------------------------------------------------------- 1 | attributes[$key] = $value; 21 | } 22 | 23 | /** 24 | * Unset an attribute on the entity. 25 | * 26 | * @param string $key 27 | * 28 | * @return void 29 | */ 30 | public function __unset($key) 31 | { 32 | unset($this->attributes[$key]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/LengthAwareEntityPaginator.php: -------------------------------------------------------------------------------- 1 | query->get(); 17 | 18 | $this->cacheRelation($results, $relation); 19 | 20 | return $results; 21 | } 22 | 23 | /** 24 | * Match the eagerly loaded results to their parents. 25 | * 26 | * @param array $results 27 | * @param string $relation 28 | * 29 | * @return array 30 | */ 31 | public function match(array $results, $relation) 32 | { 33 | return $this->matchMany($results, $relation); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Relationships/MorphMany.php: -------------------------------------------------------------------------------- 1 | query->get(); 17 | 18 | $this->cacheRelation($results, $relation); 19 | 20 | return $results; 21 | } 22 | 23 | /** 24 | * Match the eagerly loaded results to their parents. 25 | * 26 | * @param array $results 27 | * @param string $relation 28 | * 29 | * @return array 30 | */ 31 | public function match(array $results, $relation) 32 | { 33 | return $this->matchMany($results, $relation); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Relationships/MorphOne.php: -------------------------------------------------------------------------------- 1 | query->first(); 17 | 18 | $this->cacheRelation($result, $relation); 19 | 20 | return $result; 21 | } 22 | 23 | /** 24 | * Match the eagerly loaded results to their parents. 25 | * 26 | * @param array $results 27 | * @param string $relation 28 | * 29 | * @return array 30 | */ 31 | public function match(array $results, $relation) 32 | { 33 | return $this->matchOne($results, $relation); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exceptions/EntityNotFoundException.php: -------------------------------------------------------------------------------- 1 | entity = $entity; 26 | 27 | $this->message = "No query results for entity [{$entity}]."; 28 | 29 | return $this; 30 | } 31 | 32 | /** 33 | * Get the affected Entity. 34 | * 35 | * @return string 36 | */ 37 | public function getEntity() 38 | { 39 | return $this->entity; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Drivers/IlluminateConnectionProvider.php: -------------------------------------------------------------------------------- 1 | db = $db; 25 | } 26 | 27 | /** 28 | * Get a Database connection object. 29 | * 30 | * @param string $name 31 | * 32 | * @return \Illuminate\Database\Connection 33 | */ 34 | public function connection(string $name = null): Connection 35 | { 36 | return $this->db->connection($name); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Drivers/CapsuleConnectionProvider.php: -------------------------------------------------------------------------------- 1 | capsule = $capsule; 25 | } 26 | 27 | /** 28 | * Get a Database connection object. 29 | * 30 | * @param string $name 31 | * 32 | * @return \Illuminate\Database\Connection 33 | */ 34 | public function connection(string $name = null): Connection 35 | { 36 | return $this->capsule->getConnection($name); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Notifications/NotificationMap.php: -------------------------------------------------------------------------------- 1 | morphMany(DatabaseNotification::class, 'notifiable') 15 | ->orderBy('created_at', 'desc'); 16 | } 17 | 18 | /** 19 | * Get the entity's read notifications. 20 | */ 21 | public function readNotifications() 22 | { 23 | return $this->notifications() 24 | ->whereNotNull('read_at'); 25 | } 26 | 27 | /** 28 | * Get the entity's unread notifications. 29 | */ 30 | public function unreadNotifications() 31 | { 32 | return $this->notifications() 33 | ->whereNull('read_at'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Commands/Command.php: -------------------------------------------------------------------------------- 1 | aggregate = $aggregate; 33 | 34 | $this->query = $query->from($aggregate->getEntityMap()->getTable()); 35 | } 36 | 37 | abstract public function execute(); 38 | } 39 | -------------------------------------------------------------------------------- /src/Drivers/Manager.php: -------------------------------------------------------------------------------- 1 | drivers[$driver->getName()] = $driver; 24 | } 25 | 26 | /** 27 | * Get the DBAdapter. 28 | * 29 | * @param string $driver 30 | * @param string $connection connection name for drivers supporting multiple connection. 31 | * 32 | * @return DBAdapter 33 | */ 34 | public function getAdapter(string $driver, string $connection = null) 35 | { 36 | if (array_key_exists($driver, $this->drivers)) { 37 | return $this->drivers[$driver]->getAdapter($connection); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Drivers/IlluminateDriver.php: -------------------------------------------------------------------------------- 1 | connectionProvider = $connectionProvider; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getName(): string 28 | { 29 | return 'illuminate'; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function getAdapter(string $connection = null): DBAdapter 36 | { 37 | $connection = $this->connectionProvider->connection($connection); 38 | 39 | return new IlluminateDBAdapter($connection); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Relationships/HasOne.php: -------------------------------------------------------------------------------- 1 | query->first(); 17 | 18 | $this->cacheRelation($result, $relation); 19 | 20 | return $result; 21 | } 22 | 23 | /** 24 | * Get the results of the relationship. 25 | * 26 | * @return mixed 27 | */ 28 | public function fetch() 29 | { 30 | return $this->query->first(); 31 | } 32 | 33 | /** 34 | * Match the eagerly loaded results to their parents. 35 | * 36 | * @param array $results 37 | * @param string $relation 38 | * 39 | * @return array 40 | */ 41 | public function match(array $results, $relation) 42 | { 43 | return $this->matchOne($results, $relation); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Rémi Collin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Commands/Delete.php: -------------------------------------------------------------------------------- 1 | aggregate; 20 | 21 | $entity = $aggregate->getEntityObject(); 22 | $wrappedEntity = $aggregate->getWrappedEntity(); 23 | $mapper = $aggregate->getMapper(); 24 | 25 | if ($mapper->fireEvent('deleting', $wrappedEntity) === false) { 26 | return false; 27 | } 28 | 29 | $keyName = $aggregate->getEntityMap()->getKeyName(); 30 | 31 | $id = $this->aggregate->getEntityKeyValue(); 32 | 33 | if (is_null($id)) { 34 | throw new MappingException('Executed a delete command on an entity with "null" as primary key'); 35 | } 36 | 37 | $this->query->where($keyName, '=', $id)->delete(); 38 | 39 | $mapper->fireEvent('deleted', $wrappedEntity, false); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Plugins/SoftDeletes/Restore.php: -------------------------------------------------------------------------------- 1 | aggregate; 17 | $entity = $aggregate->getEntityObject(); 18 | $mapper = $aggregate->getMapper(); 19 | $entityMap = $mapper->getEntityMap(); 20 | 21 | if ($mapper->fireEvent('restoring', $entity) === false) { 22 | return false; 23 | } 24 | 25 | $keyName = $entityMap->getKeyName(); 26 | 27 | $query = $this->query->where($keyName, '=', $aggregate->getEntityAttribute($keyName)); 28 | 29 | $deletedAtColumn = $entityMap->getQualifiedDeletedAtColumn(); 30 | 31 | $query->update([$deletedAtColumn => null]); 32 | 33 | $aggregate->setEntityAttribute($deletedAtColumn, null); 34 | 35 | $mapper->fireEvent('restored', $entity, false); 36 | 37 | $mapper->getEntityCache()->refresh($aggregate); 38 | 39 | return $entity; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Drivers/DBAdapter.php: -------------------------------------------------------------------------------- 1 | createFactory()->getHydratorClass(); 30 | $hydrator = new $hydratorClass(); 31 | 32 | if ($manager->isValueObject($object)) { 33 | $entityMap = $manager->getValueMap($object); 34 | } else { 35 | $entityMap = $manager->mapper($object)->getEntityMap(); 36 | } 37 | 38 | // Build Wrapper 39 | return new ObjectWrapper($object, $entityMap, $hydrator); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Relationships/MorphPivot.php: -------------------------------------------------------------------------------- 1 | where($this->morphType, $this->morphClass); 37 | 38 | return parent::setKeysForSaveQuery($query); 39 | } 40 | 41 | /** 42 | * Set the morph type for the pivot. 43 | * 44 | * @param string $morphType 45 | * 46 | * @return self 47 | */ 48 | public function setMorphType($morphType) 49 | { 50 | $this->morphType = $morphType; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * Set the morph class for the pivot. 57 | * 58 | * @param string $morphClass 59 | * 60 | * @return self 61 | */ 62 | public function setMorphClass($morphClass) 63 | { 64 | $this->morphClass = $morphClass; 65 | 66 | return $this; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/MagicGetters.php: -------------------------------------------------------------------------------- 1 | Solution would be to access the entityMap's $attributes, but we 31 | // have to do this in a very efficient way. 32 | // 33 | // Manager::getEntityMap(get_class($this))->hasProperty() 34 | // 35 | // We could do the casting to array / json the same way, and it would 36 | 37 | if (property_exists($this, $key)) { 38 | return $this->$key; 39 | } 40 | 41 | if (array_key_exists($key, $this->attributes)) { 42 | return $this->attributes[$key]; 43 | } 44 | } 45 | 46 | /** 47 | * Determine if an attribute exists on the entity. 48 | * 49 | * @param string $key 50 | * 51 | * @return bool 52 | */ 53 | public function __isset($key) 54 | { 55 | return array_key_exists($key, $this->attributes) || property_exists($this, $key); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/System/InternallyMappable.php: -------------------------------------------------------------------------------- 1 | attributes = $attributes; 31 | } 32 | 33 | /** 34 | * Method used by the mapper to get the 35 | * raw object's values. 36 | * 37 | * @return array 38 | */ 39 | public function getEntityAttributes() 40 | { 41 | return $this->attributes; 42 | } 43 | 44 | /** 45 | * Method used by the mapper to set raw 46 | * key-value pair. 47 | * 48 | * @param string $key 49 | * @param string $value 50 | * 51 | * @return void 52 | */ 53 | public function setEntityAttribute($key, $value) 54 | { 55 | $this->attributes[$key] = $value; 56 | } 57 | 58 | /** 59 | * Method used by the mapper to get single 60 | * key-value pair. 61 | * 62 | * @param string $key 63 | * 64 | * @return mixed|null 65 | */ 66 | public function getEntityAttribute($key) 67 | { 68 | if (array_key_exists($key, $this->attributes)) { 69 | return $this->attributes[$key]; 70 | } else { 71 | return; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/System/Cache/CachedRelationship.php: -------------------------------------------------------------------------------- 1 | hash = $hash; 34 | $this->pivotAttributes = $pivotAttributes; 35 | } 36 | 37 | /** 38 | * Return true if any pivot attributes are present. 39 | * 40 | * @return bool 41 | */ 42 | public function hasPivotAttributes(): bool 43 | { 44 | return !empty($this->pivotAttributes); 45 | } 46 | 47 | /** 48 | * Returns the hash of the related entity. 49 | * 50 | * @return string 51 | */ 52 | public function getHash(): string 53 | { 54 | return $this->hash; 55 | } 56 | 57 | /** 58 | * Get the cached values for the pivot attributes. 59 | * 60 | * @return array 61 | */ 62 | public function getPivotAttributes(): array 63 | { 64 | return $this->pivotAttributes; 65 | } 66 | 67 | /** 68 | * Access to the hash for fast cache comparison. 69 | * 70 | * @return string 71 | */ 72 | public function __toString(): string 73 | { 74 | return $this->hash; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Drivers/IlluminateDBAdapter.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function getQuery() 34 | { 35 | $connection = $this->connection; 36 | 37 | $grammar = $connection->getQueryGrammar(); 38 | 39 | return new IlluminateQueryBuilder($connection, $grammar, $connection->getPostProcessor()); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function getDateFormat(): string 46 | { 47 | return $this->connection->getQueryGrammar()->getDateFormat(); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function beginTransaction() 54 | { 55 | $this->connection->beginTransaction(); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function commit() 62 | { 63 | $this->connection->commit(); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function rollback() 70 | { 71 | $this->connection->rollBack(); 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function fromDatabase(array $rows): array 78 | { 79 | return $rows; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/System/Cache/InstanceCache.php: -------------------------------------------------------------------------------- 1 | class = $class; 31 | } 32 | 33 | /** 34 | * Add an entity to the cache. 35 | * 36 | * @param mixed $entity 37 | * @param string $id 38 | * 39 | * @throws CacheException 40 | * 41 | * @return void 42 | */ 43 | public function add($entity, string $id) 44 | { 45 | $entityClass = get_class($entity); 46 | 47 | if ($entityClass !== $this->class) { 48 | throw new CacheException('Tried to cache an instance with a wrong type : expected '.$this->class.", got $entityClass"); 49 | } 50 | 51 | // Cache once and ignore subsequent caching 52 | // attempts if the entity is already stored 53 | if (!$this->has($id)) { 54 | $this->instances[$id] = $entity; 55 | } 56 | } 57 | 58 | /** 59 | * Check if an instance exists in the cache. 60 | * 61 | * @param string $id 62 | * 63 | * @return bool 64 | */ 65 | public function has(string $id): bool 66 | { 67 | return array_key_exists($id, $this->instances); 68 | } 69 | 70 | /** 71 | * Return an entity's instance. 72 | * 73 | * @param string $id 74 | * 75 | * @return mixed|null 76 | */ 77 | public function get(string $id) 78 | { 79 | if ($this->has($id)) { 80 | return $this->instances[$id]; 81 | } 82 | } 83 | 84 | /** 85 | * Clear the cache. 86 | * 87 | * @return void 88 | */ 89 | public function clear() 90 | { 91 | $this->instances = []; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "analogue/orm", 3 | "description": "An intuitive Data Mapper ORM for PHP and Laravel", 4 | "keywords": [ 5 | "orm", 6 | "datamapper", 7 | "laravel", 8 | "entity", 9 | "repository", 10 | "mapper" 11 | ], 12 | "homepage": "http://github.com/analogueorm/analogue", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Rémi Collin", 17 | "email": "remi@code16.fr" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=7.0.0", 22 | "illuminate/database": "5.5.*|5.6.*", 23 | "illuminate/events": "5.5.*|5.6.*", 24 | "illuminate/pagination": "5.5.*|5.6.*", 25 | "doctrine/instantiator": "^1.0.0", 26 | "ocramius/proxy-manager": "^2.0.0", 27 | "ocramius/generated-hydrator": "^2.0.0", 28 | "psr/simple-cache": "~1.0", 29 | "psr/container": "~1.0" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "~6.0", 33 | "laravel/laravel": "5.5.*|5.6.*", 34 | "fzaninotto/faker": "~1.4", 35 | "mockery/mockery": "0.9.*", 36 | "symfony/css-selector": "2.8.*|3.0.*|4.0.*", 37 | "symfony/dom-crawler": "2.8.*|3.0.*|4.0.*", 38 | "analogue/factory": "1.2.*|1.3.*", 39 | "laravel/browser-kit-testing": "^1.0" 40 | }, 41 | "suggest": { 42 | "analogue/laravel-auth": "Analogue's authentication driver for Laravel.", 43 | "analogue/factory": "Create simple dummy entities for your tests", 44 | "analogue/mongodb": "MongoDB driver for Analogue ORM" 45 | }, 46 | "autoload": { 47 | "psr-4": { 48 | "Analogue\\ORM\\": "src/" 49 | }, 50 | "files": [ 51 | "src/helpers.php" 52 | ] 53 | }, 54 | "autoload-dev": { 55 | "classmap": [ 56 | "tests/AnalogueTestCase.php", 57 | "tests/DomainTestCase.php", 58 | "tests/ClassFinder.php" 59 | ], 60 | "psr-4": { 61 | "TestApp\\": "tests/src" 62 | }, 63 | "files": [ 64 | "tests/helpers.php" 65 | ] 66 | }, 67 | "extra": { 68 | "laravel": { 69 | "providers": [ 70 | "Analogue\\ORM\\AnalogueServiceProvider" 71 | ], 72 | "aliases": { 73 | "Analogue": "Analogue\\ORM\\AnalogueFacade" 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/System/Proxies/ProxyFactory.php: -------------------------------------------------------------------------------- 1 | getEntityMap(); 21 | 22 | $singleRelations = $entityMap->getSingleRelationships(); 23 | $manyRelations = $entityMap->getManyRelationships(); 24 | 25 | if (in_array($relation, $singleRelations)) { 26 | return $this->makeEntityProxy($entity, $relation, $class); 27 | } 28 | 29 | if (in_array($relation, $manyRelations)) { 30 | return new CollectionProxy($entity, $relation); 31 | } 32 | 33 | throw new MappingException("Could not identify relation '$relation'"); 34 | } 35 | 36 | /** 37 | * Create an instance of a proxy object, extending the actual 38 | * related class. 39 | * 40 | * @param mixed $entity parent object 41 | * @param string $relation the name of the relationship method 42 | * @param string $class the class name of the related object 43 | * 44 | * @return mixed 45 | */ 46 | protected function makeEntityProxy($entity, $relation, $class) 47 | { 48 | $proxyPath = Manager::getInstance()->getProxyPath(); 49 | 50 | if ($proxyPath !== null) { 51 | $proxyConfig = new Configuration(); 52 | $proxyConfig->setProxiesTargetDir($proxyPath); 53 | 54 | $factory = new LazyLoadingValueHolderFactory($proxyConfig); 55 | } else { 56 | $factory = new LazyLoadingValueHolderFactory(); 57 | } 58 | 59 | $initializer = function (&$wrappedObject, LazyLoadingInterface $proxy, $method, array $parameters, &$initializer) use ($entity, $relation) { 60 | $entityMap = Manager::getMapper($entity)->getEntityMap(); 61 | 62 | $wrappedObject = $entityMap->$relation($entity)->getResults($relation); 63 | 64 | $initializer = null; // disable initialization 65 | return true; // confirm that initialization occurred correctly 66 | }; 67 | 68 | return $factory->createProxy($class, $initializer); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/System/MapperFactory.php: -------------------------------------------------------------------------------- 1 | drivers = $drivers; 47 | 48 | $this->dispatcher = $dispatcher; 49 | 50 | $this->manager = $manager; 51 | } 52 | 53 | /** 54 | * Return a new Mapper instance. 55 | * 56 | * @param string $entityClass 57 | * @param EntityMap $entityMap 58 | * 59 | * @return Mapper 60 | */ 61 | public function make($entityClass, EntityMap $entityMap) 62 | { 63 | $driver = $entityMap->getDriver(); 64 | 65 | $connection = $entityMap->getConnection(); 66 | 67 | $adapter = $this->drivers->getAdapter($driver, $connection); 68 | 69 | $entityMap->setDateFormat($adapter->getDateFormat()); 70 | 71 | $mapper = new Mapper($entityMap, $adapter, $this->dispatcher, $this->manager); 72 | 73 | // Fire Initializing Event 74 | $mapper->fireEvent('initializing', $mapper); 75 | 76 | // Proceed necessary parsing on the EntityMap object 77 | if (!$entityMap->isBooted()) { 78 | $entityMap->initialize(); 79 | } 80 | 81 | // Apply Inheritance scope, if necessary 82 | if ($entityMap->getInheritanceType() == 'single_table') { 83 | $scope = new SingleTableInheritanceScope($entityMap); 84 | $mapper->addGlobalScope($scope); 85 | } 86 | 87 | // Fire Initialized Event 88 | $mapper->fireEvent('initialized', $mapper); 89 | 90 | return $mapper; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/AnalogueServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('analogue', function ($app) { 40 | $db = $app['db']; 41 | 42 | $connectionProvider = new IlluminateConnectionProvider($db); 43 | 44 | $illuminate = new IlluminateDriver($connectionProvider); 45 | 46 | $driverManager = new DriverManager(); 47 | 48 | $driverManager->addDriver($illuminate); 49 | 50 | $event = $app->make('events'); 51 | 52 | $manager = new Manager($driverManager, $event); 53 | 54 | $manager->registerPlugin(\Analogue\ORM\Plugins\Timestamps\TimestampsPlugin::class); 55 | $manager->registerPlugin(\Analogue\ORM\Plugins\SoftDeletes\SoftDeletesPlugin::class); 56 | 57 | // If the cache is pre laravel 5.5, it doesn't implements PSR-16, so we'll skip it. 58 | $cache = $app->make(CacheRepository::class); 59 | 60 | if ($cache instanceof CacheInterface) { 61 | $manager->setCache($cache); 62 | } 63 | 64 | $proxyPath = storage_path('framework/analogue/proxies'); 65 | 66 | if (!file_exists($proxyPath)) { 67 | mkdir($proxyPath, 0777, true); 68 | } 69 | 70 | $proxyConfig = new \ProxyManager\Configuration(); 71 | $proxyConfig->setProxiesTargetDir($proxyPath); 72 | spl_autoload_register($proxyConfig->getProxyAutoloader()); 73 | 74 | $manager->setProxyPath($proxyPath); 75 | 76 | return $manager; 77 | }); 78 | 79 | $this->app->bind(Manager::class, function ($app) { 80 | return $app->make('analogue'); 81 | }); 82 | } 83 | 84 | /** 85 | * Get the services provided by the provider. 86 | * 87 | * @return array 88 | */ 89 | public function provides() 90 | { 91 | return ['analogue']; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Plugins/Timestamps/TimestampsPlugin.php: -------------------------------------------------------------------------------- 1 | manager->registerGlobalEvent('initialized', function ($event, $payload = null) { 21 | 22 | // Cross Compatible Event handling with 5.3 23 | // TODO : find a replacement event handler 24 | if (is_null($payload)) { 25 | $mapper = $event; 26 | } else { 27 | $mapper = $payload[0]->mapper; 28 | } 29 | 30 | $entityMap = $mapper->getEntityMap(); 31 | 32 | if ($entityMap->usesTimestamps()) { 33 | $mapper->registerEvent('creating', function ($event) use ($entityMap) { 34 | $entity = $event->entity; 35 | $wrappedEntity = $this->getMappable($entity); 36 | 37 | $createdAtField = $entityMap->getCreatedAtColumn(); 38 | $updatedAtField = $entityMap->getUpdatedAtColumn(); 39 | 40 | $time = new Carbon(); 41 | 42 | if (is_null($wrappedEntity->getEntityAttribute($createdAtField))) { 43 | $wrappedEntity->setEntityAttribute($createdAtField, $time); 44 | $wrappedEntity->setEntityAttribute($updatedAtField, $time); 45 | } 46 | }); 47 | 48 | $mapper->registerEvent('updating', function ($event) use ($entityMap) { 49 | $entity = $event->entity; 50 | $wrappedEntity = $this->getMappable($entity); 51 | 52 | $updatedAtField = $entityMap->getUpdatedAtColumn(); 53 | 54 | $time = new Carbon(); 55 | 56 | $wrappedEntity->setEntityAttribute($updatedAtField, $time); 57 | }); 58 | } 59 | }); 60 | } 61 | 62 | /** 63 | * Return internally mappable if not mappable. 64 | * 65 | * @param mixed $entity 66 | * 67 | * @return InternallyMappable 68 | */ 69 | protected function getMappable($entity): InternallyMappable 70 | { 71 | if ($entity instanceof InternallyMappable) { 72 | return $entity; 73 | } 74 | 75 | $factory = new Factory(); 76 | $wrappedEntity = $factory->make($entity); 77 | 78 | return $wrappedEntity; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function getCustomEvents(): array 85 | { 86 | return []; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/MagicCasting.php: -------------------------------------------------------------------------------- 1 | $offset); 21 | } 22 | 23 | /** 24 | * Get the value for a given offset. 25 | * 26 | * @param mixed $offset 27 | * 28 | * @return mixed 29 | */ 30 | public function offsetGet($offset) 31 | { 32 | return $this->$offset; 33 | } 34 | 35 | /** 36 | * Set the value for a given offset. 37 | * 38 | * @param mixed $offset 39 | * @param mixed $value 40 | * 41 | * @return void 42 | */ 43 | public function offsetSet($offset, $value) 44 | { 45 | $this->$offset = $value; 46 | } 47 | 48 | /** 49 | * Unset the value for a given offset. 50 | * 51 | * @param mixed $offset 52 | * 53 | * @return void 54 | */ 55 | public function offsetUnset($offset) 56 | { 57 | unset($this->$offset); 58 | } 59 | 60 | /** 61 | * Convert the object into something JSON serializable. 62 | * 63 | * @return array 64 | */ 65 | public function jsonSerialize() 66 | { 67 | return $this->toArray(); 68 | } 69 | 70 | /** 71 | * Convert the entity instance to JSON. 72 | * 73 | * @param int $options 74 | * 75 | * @return string 76 | */ 77 | public function toJson($options = 0) 78 | { 79 | return json_encode($this->toArray(), $options); 80 | } 81 | 82 | /** 83 | * Convert Mappable object to array;. 84 | * 85 | * @return array 86 | */ 87 | public function toArray() 88 | { 89 | return $this->attributesToArray($this->attributes); 90 | } 91 | 92 | /** 93 | * Transform the Object to array/json,. 94 | * 95 | * @param array $sourceAttributes 96 | * 97 | * @return array 98 | */ 99 | protected function attributesToArray(array $sourceAttributes) 100 | { 101 | $attributes = []; 102 | 103 | foreach ($sourceAttributes as $key => $attribute) { 104 | // If the attribute is a proxy, and hasn't be loaded, we discard 105 | // it from the returned set. 106 | if ($attribute instanceof ProxyInterface && !$attribute->isProxyInitialized()) { 107 | continue; 108 | } 109 | 110 | if ($attribute instanceof Carbon) { 111 | $attributes[$key] = $attribute->__toString(); 112 | continue; 113 | } 114 | 115 | if ($attribute instanceof Arrayable) { 116 | $attributes[$key] = $attribute->toArray(); 117 | } else { 118 | $attributes[$key] = $attribute; 119 | } 120 | } 121 | 122 | return $attributes; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Plugins/SoftDeletes/SoftDeletingScope.php: -------------------------------------------------------------------------------- 1 | getMapper()->getEntityMap(); 26 | 27 | $query->whereNull($entityMap->getQualifiedDeletedAtColumn()); 28 | 29 | $this->extend($query); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function remove(Query $query) 36 | { 37 | $column = $query->getMapper()->getEntityMap()->getQualifiedDeletedAtColumn(); 38 | 39 | $query = $query->getQuery(); 40 | 41 | foreach ((array) $query->wheres as $key => $where) { 42 | // If the where clause is a soft delete date constraint, we will remove it from 43 | // the query and reset the keys on the wheres. This allows this developer to 44 | // include deleted model in a relationship result set that is lazy loaded. 45 | if ($this->isSoftDeleteConstraint($where, $column)) { 46 | unset($query->wheres[$key]); 47 | 48 | $query->wheres = array_values($query->wheres); 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * Extend the query builder with the needed functions. 55 | * 56 | * @param \Analogue\ORM\System\Query $query 57 | * 58 | * @return void 59 | */ 60 | public function extend(Query $query) 61 | { 62 | foreach ($this->extensions as $extension) { 63 | $this->{"add{$extension}"}($query); 64 | } 65 | } 66 | 67 | /** 68 | * Add the with-trashed extension to the builder. 69 | * 70 | * @param \Analogue\ORM\System\Query $query 71 | * 72 | * @return void 73 | */ 74 | protected function addWithTrashed(Query $query) 75 | { 76 | $query->macro('withTrashed', function (Query $query) { 77 | $this->remove($query); 78 | 79 | return $query; 80 | }); 81 | } 82 | 83 | /** 84 | * Add the only-trashed extension to the builder. 85 | * 86 | * @param \Analogue\ORM\System\Query $query 87 | * 88 | * @return void 89 | */ 90 | protected function addOnlyTrashed(Query $query) 91 | { 92 | $query->macro('onlyTrashed', function (Query $query) { 93 | $this->remove($query); 94 | 95 | $query->getQuery()->whereNotNull($query->getMapper()->getEntityMap()->getQualifiedDeletedAtColumn()); 96 | 97 | return $query; 98 | }); 99 | } 100 | 101 | /** 102 | * Determine if the given where clause is a soft delete constraint. 103 | * 104 | * @param array $where 105 | * @param string $column 106 | * 107 | * @return bool 108 | */ 109 | protected function isSoftDeleteConstraint(array $where, $column) 110 | { 111 | return $where['type'] == 'Null' && $where['column'] == $column; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/System/SingleTableInheritanceScope.php: -------------------------------------------------------------------------------- 1 | column = $entityMap->getDiscriminatorColumn(); 30 | 31 | // First we need to retrieve the base class & it's normalized 32 | // type string 33 | $class = $entityMap->getClass(); 34 | $this->types[] = $this->getTypeStringForEntity($class, $entityMap); 35 | 36 | // Then, we parse all registered entities for any potential 37 | // child class. 38 | $classes = Manager::getInstance()->getRegisteredEntities(); 39 | 40 | foreach ($classes as $otherClass => $entityMap) { 41 | if (is_subclass_of($otherClass, $class)) { 42 | $this->types[] = $this->getTypeStringForEntity($otherClass, $entityMap); 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * Get the normalized value to use for query on discriminator column. 49 | * 50 | * @param string $class 51 | * @param EntityMap $entityMap 52 | * 53 | * @return string 54 | */ 55 | protected function getTypeStringForEntity($class, EntityMap $entityMap) 56 | { 57 | $class = $entityMap->getClass(); 58 | 59 | $type = array_keys( 60 | $entityMap->getDiscriminatorColumnMap(), 61 | $class 62 | ); 63 | 64 | if (count($type) == 0) { 65 | return $class; 66 | } 67 | 68 | return $type[0]; 69 | } 70 | 71 | /** 72 | * Apply the scope to a given Analogue query builder. 73 | * 74 | * @param \Analogue\ORM\System\Query $query 75 | * 76 | * @return void 77 | */ 78 | public function apply(Query $query) 79 | { 80 | $query->whereIn($this->column, $this->types); 81 | } 82 | 83 | /** 84 | * Remove the scope from the given Analogue query builder. 85 | * 86 | * @param mixed $query 87 | * 88 | * @return void 89 | */ 90 | public function remove(Query $query) 91 | { 92 | $query = $query->getQuery(); 93 | 94 | foreach ((array) $query->wheres as $key => $where) { 95 | if ($this->isSingleTableConstraint($where, $this->column)) { 96 | unset($query->wheres[$key]); 97 | 98 | $query->wheres = array_values($query->wheres); 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * Determine if the given where clause is a single table inheritance constraint. 105 | * 106 | * @param array $where 107 | * @param string $column 108 | * 109 | * @return bool 110 | */ 111 | protected function isSingleTableConstraint(array $where, $column) 112 | { 113 | return $where['column'] == $column; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Analogue.php: -------------------------------------------------------------------------------- 1 | addConnection($connection); 51 | 52 | $this->boot(); 53 | } 54 | } 55 | 56 | /** 57 | * Boot Analogue. 58 | * 59 | * @return Analogue 60 | */ 61 | public function boot() 62 | { 63 | if (static::$booted) { 64 | return $this; 65 | } 66 | 67 | $dispatcher = new Dispatcher(); 68 | 69 | $connectionProvider = new CapsuleConnectionProvider(static::$capsule); 70 | 71 | $illuminate = new IlluminateDriver($connectionProvider); 72 | 73 | $driverManager = new DriverManager(); 74 | 75 | $driverManager->addDriver($illuminate); 76 | 77 | static::$manager = new Manager($driverManager, $dispatcher); 78 | 79 | static::$instance = $this; 80 | 81 | static::$booted = true; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Add a connection array to Capsule. 88 | * 89 | * @param array $config 90 | * @param string $name 91 | */ 92 | public function addConnection($config, $name = 'default') 93 | { 94 | static::$capsule->addConnection($config, $name); 95 | } 96 | 97 | /** 98 | * Get a Database connection object. 99 | * 100 | * @param $name 101 | * 102 | * @return \Illuminate\Database\Connection 103 | */ 104 | public function connection($name = null) 105 | { 106 | return static::$capsule->getConnection($name); 107 | } 108 | 109 | /** 110 | * Dynamically handle static calls to the instance, Facade Style. 111 | * 112 | * @param string $method 113 | * @param array $parameters 114 | * 115 | * @return mixed 116 | */ 117 | public static function __callStatic($method, $parameters) 118 | { 119 | return call_user_func_array([static::$instance, $method], $parameters); 120 | } 121 | 122 | /** 123 | * Dynamically handle calls to the Analogue Manager instance. 124 | * 125 | * @param string $method 126 | * @param array $parameters 127 | * 128 | * @return mixed 129 | */ 130 | public function __call($method, $parameters) 131 | { 132 | return call_user_func_array([static::$manager, $method], $parameters); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Relationships/MorphOneOrMany.php: -------------------------------------------------------------------------------- 1 | morphType = $type; 38 | $this->morphClass = $mapper->getManager()->getMapper($parent)->getEntityMap()->getMorphClass(); 39 | 40 | parent::__construct($mapper, $parent, $id, $localKey); 41 | } 42 | 43 | /** 44 | * Set the base constraints on the relation query. 45 | * 46 | * @return void 47 | */ 48 | public function addConstraints() 49 | { 50 | if (static::$constraints) { 51 | parent::addConstraints(); 52 | 53 | $this->query->where($this->morphType, $this->morphClass); 54 | } 55 | } 56 | 57 | /** 58 | * Get the relationship count query. 59 | * 60 | * @param Query $query 61 | * @param Query $parent 62 | * 63 | * @return Query 64 | */ 65 | public function getRelationCountQuery(Query $query, Query $parent) 66 | { 67 | $query = parent::getRelationCountQuery($query, $parent); 68 | 69 | return $query->where($this->morphType, $this->morphClass); 70 | } 71 | 72 | /** 73 | * Set the constraints for an eager load of the relation. 74 | * 75 | * @param array $results 76 | * 77 | * @return void 78 | */ 79 | public function addEagerConstraints(array $results) 80 | { 81 | parent::addEagerConstraints($results); 82 | 83 | $this->query->where($this->morphType, $this->morphClass); 84 | } 85 | 86 | /** 87 | * Get the foreign key "type" name. 88 | * 89 | * @return string 90 | */ 91 | public function getMorphType() 92 | { 93 | return $this->morphType; 94 | } 95 | 96 | /** 97 | * Get the plain morph type name without the table. 98 | * 99 | * @return string 100 | */ 101 | public function getPlainMorphType() 102 | { 103 | return last(explode('.', $this->morphType)); 104 | } 105 | 106 | /** 107 | * Get the class name of the parent model. 108 | * 109 | * @return string 110 | */ 111 | public function getMorphClass() 112 | { 113 | return $this->morphClass; 114 | } 115 | 116 | /** 117 | * Get the foreign key as value pair for this relation. 118 | * 119 | * @return array 120 | */ 121 | public function getForeignKeyValuePair() 122 | { 123 | return [ 124 | $this->getPlainForeignKey() => $this->getParentKey(), 125 | $this->getPlainMorphType() => $this->morphClass, 126 | ]; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Entity.php: -------------------------------------------------------------------------------- 1 | hasGetMutator($key)) { 26 | $method = 'get'.$this->getMutatorMethod($key); 27 | 28 | $attribute = null; 29 | 30 | if (isset($this->attributes[$key])) { 31 | $attribute = $this->attributes[$key]; 32 | } 33 | 34 | return $this->$method($attribute); 35 | } 36 | if (!array_key_exists($key, $this->attributes)) { 37 | return; 38 | } 39 | 40 | return $this->attributes[$key]; 41 | } 42 | 43 | /** 44 | * Dynamically set attributes on the entity. 45 | * 46 | * @param string $key 47 | * @param mixed $value 48 | * 49 | * @return void 50 | */ 51 | public function __set($key, $value) 52 | { 53 | if ($this->hasSetMutator($key)) { 54 | $method = 'set'.$this->getMutatorMethod($key); 55 | 56 | $this->$method($value); 57 | } else { 58 | $this->attributes[$key] = $value; 59 | } 60 | } 61 | 62 | /** 63 | * Is a getter method defined ? 64 | * 65 | * @param string $key 66 | * 67 | * @return bool 68 | */ 69 | protected function hasGetMutator($key) 70 | { 71 | return method_exists($this, 'get'.$this->getMutatorMethod($key)); 72 | } 73 | 74 | /** 75 | * Is a setter method defined ? 76 | * 77 | * @param string $key 78 | * 79 | * @return bool 80 | */ 81 | protected function hasSetMutator($key) 82 | { 83 | return method_exists($this, 'set'.$this->getMutatorMethod($key)); 84 | } 85 | 86 | /** 87 | * @param $key 88 | * 89 | * @return string 90 | */ 91 | protected function getMutatorMethod($key) 92 | { 93 | $key = ucwords(str_replace(['-', '_'], ' ', $key)); 94 | 95 | return str_replace(' ', '', $key).'Attribute'; 96 | } 97 | 98 | /** 99 | * Convert every attributes to value / arrays. 100 | * 101 | * @return array 102 | */ 103 | public function toArray() 104 | { 105 | // First, call the trait method before filtering 106 | // with Entity specific methods 107 | $attributes = $this->attributesToArray($this->attributes); 108 | 109 | foreach ($this->attributes as $key => $attribute) { 110 | if (in_array($key, $this->hidden)) { 111 | unset($attributes[$key]); 112 | continue; 113 | } 114 | if ($this->hasGetMutator($key)) { 115 | $method = 'get'.$this->getMutatorMethod($key); 116 | $attributes[$key] = $this->$method($attribute); 117 | } 118 | } 119 | 120 | return $attributes; 121 | } 122 | 123 | /** 124 | * Fill an entity with key-value pairs. 125 | * 126 | * @param array $attributes 127 | * 128 | * @return void 129 | */ 130 | public function fill(array $attributes) 131 | { 132 | foreach ($attributes as $key => $attribute) { 133 | $this->{$key} = $attribute; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Plugins/SoftDeletes/SoftDeletesPlugin.php: -------------------------------------------------------------------------------- 1 | manager->registerGlobalEvent('initialized', function ($event, $payload = null) { 24 | 25 | // Cross Compatible Event handling with 5.3 26 | // TODO : find a replacement event handler 27 | if (is_null($payload)) { 28 | $mapper = $event; 29 | } else { 30 | $mapper = $payload[0]->mapper; 31 | } 32 | 33 | $entityMap = $mapper->getEntityMap(); 34 | 35 | if ($entityMap->usesSoftDeletes()) { 36 | $this->registerSoftDelete($mapper); 37 | 38 | foreach ($this->getCustomEvents() as $name => $class) { 39 | $mapper->addCustomEvent($name, $class); 40 | } 41 | } 42 | }); 43 | } 44 | 45 | /** 46 | * By hooking to the mapper initialization event, we can extend it 47 | * with the softDelete capacity. 48 | * 49 | * @param \Analogue\ORM\System\Mapper $mapper 50 | * 51 | * @throws \Analogue\ORM\Exceptions\MappingException 52 | * 53 | * @return bool|void 54 | */ 55 | protected function registerSoftDelete(Mapper $mapper) 56 | { 57 | $entityMap = $mapper->getEntityMap(); 58 | 59 | // Add Scopes 60 | $mapper->addGlobalScope(new SoftDeletingScope()); 61 | 62 | // Register 'deleting' events 63 | $mapper->registerEvent('deleting', function ($event) use ($entityMap) { 64 | $entity = $event->entity; 65 | $wrappedEntity = $this->getMappable($entity); 66 | 67 | $deletedAtField = $entityMap->getQualifiedDeletedAtColumn(); 68 | 69 | if (!is_null($wrappedEntity->getEntityAttribute($deletedAtField))) { 70 | return true; 71 | } 72 | 73 | $time = new Carbon(); 74 | 75 | $wrappedEntity->setEntityAttribute($deletedAtField, $time); 76 | 77 | $plainObject = $wrappedEntity->getObject(); 78 | $this->manager->mapper(get_class($plainObject))->store($plainObject); 79 | 80 | return false; 81 | }); 82 | 83 | // Register RestoreCommand 84 | $mapper->addCustomCommand(Restore::class); 85 | } 86 | 87 | /** 88 | * Return internally mappable if not mappable. 89 | * 90 | * @param mixed $entity 91 | * 92 | * @return InternallyMappable 93 | */ 94 | protected function getMappable($entity): InternallyMappable 95 | { 96 | if ($entity instanceof InternallyMappable) { 97 | return $entity; 98 | } 99 | 100 | $factory = new Factory(); 101 | $wrappedEntity = $factory->make($entity); 102 | 103 | return $wrappedEntity; 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | public function getCustomEvents(): array 110 | { 111 | return [ 112 | 'restoring' => Events\Restoring::class, 113 | 'restored' => Events\Restored::class, 114 | ]; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/System/Builders/PolymorphicResultBuilder.php: -------------------------------------------------------------------------------- 1 | defaultMapper = $defaultMapper; 54 | $this->entityMap = $defaultMapper->getEntityMap(); 55 | } 56 | 57 | /** 58 | * Convert a result set into an array of entities. 59 | * 60 | * @param array $results 61 | * @param array $eagerLoads name of the relation(s) to be eager loaded on the Entities 62 | * 63 | * @return array 64 | */ 65 | public function build(array $results, array $eagerLoads) 66 | { 67 | // Make a list of all primary key of the current result set. This will 68 | // allow us to group all polymorphic operations by type, then put 69 | // back every object in the intended order. 70 | $primaryKeyColumn = $this->entityMap->getKeyName(); 71 | $ids = array_map(function ($row) use ($primaryKeyColumn) { 72 | return $row[$primaryKeyColumn]; 73 | }, $results); 74 | 75 | $results = array_combine($ids, $results); 76 | 77 | // Make a list of types appearing within this result set. 78 | $discriminatorColumn = $this->entityMap->getDiscriminatorColumn(); 79 | $types = array_unique(array_pluck($results, $discriminatorColumn)); 80 | 81 | // We'll split the result set by type that will make it easier to deal 82 | // with. 83 | $entities = []; 84 | 85 | foreach ($types as $type) { 86 | $this->mappers[$type] = $this->getMapperForType($type); 87 | 88 | $resultsByType[$type] = array_filter($results, function (array $row) use ($type, $discriminatorColumn) { 89 | return $row[$discriminatorColumn] === $type; 90 | }); 91 | 92 | $entities = $entities + $this->buildResultsForType($resultsByType[$type], $type, $eagerLoads); 93 | } 94 | 95 | return array_map(function ($id) use ($entities) { 96 | return $entities[$id]; 97 | }, $ids); 98 | } 99 | 100 | protected function buildResultsForType($results, $type, array $eagerLoads) 101 | { 102 | $builder = new ResultBuilder($this->mappers[$type]); 103 | 104 | return $builder->build($results, $eagerLoads); 105 | } 106 | 107 | protected function getMapperForType(string $type): Mapper 108 | { 109 | $columnMap = $this->entityMap->getDiscriminatorColumnMap(); 110 | 111 | $class = isset($columnMap[$type]) ? $columnMap[$type] : $type; 112 | 113 | return Manager::getInstance()->mapper($class); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Relationships/EmbedsMany.php: -------------------------------------------------------------------------------- 1 | relation; 23 | 24 | if (!$this->asArray) { 25 | throw new MappingException("column '$column' should be of type array or json"); 26 | } 27 | 28 | return $this->matchAsArray($attributes); 29 | } 30 | 31 | /** 32 | * Match array attribute from parent to an embedded object, 33 | * and return the updated attributes. 34 | * 35 | * @param array $attributes 36 | * 37 | * @throws MappingException 38 | * 39 | * @return array 40 | */ 41 | protected function matchAsArray(array $attributes): array 42 | { 43 | // Extract the attributes with the key of the relation, 44 | // which should be an array. 45 | $key = $this->relation; 46 | 47 | if (!array_key_exists($key, $attributes)) { 48 | $attributes[$key] = $this->asJson ? json_encode([]) : []; 49 | } 50 | 51 | if ($this->asJson) { 52 | $attributes[$key] = json_decode($attributes[$key], true); 53 | } 54 | 55 | if (!is_array($attributes[$key])) { 56 | throw new MappingException("'$key' column should be an array, actual :".$attributes[$key]); 57 | } 58 | 59 | $attributes[$key] = $this->buildEmbeddedCollection($attributes[$key]); 60 | 61 | return $attributes; 62 | } 63 | 64 | /** 65 | * Build an embedded collection and returns it. 66 | * 67 | * @param array $rows 68 | * 69 | * @return Collection 70 | */ 71 | protected function buildEmbeddedCollection(array $rows): Collection 72 | { 73 | return collect($rows)->map(function ($attributes) { 74 | return $this->buildEmbeddedObject($attributes); 75 | }); 76 | } 77 | 78 | /** 79 | * Transform embedded object into db column(s). 80 | * 81 | * @param mixed $objects 82 | * 83 | * @throws MappingException 84 | * 85 | * @return array $columns 86 | */ 87 | public function normalize($objects): array 88 | { 89 | if (!$this->asArray) { 90 | throw new MappingException('Cannot normalize an embedsMany relation as row columns'); 91 | } 92 | 93 | return $this->normalizeAsArray($objects); 94 | } 95 | 96 | /** 97 | * Normalize object as array containing raw attributes. 98 | * 99 | * @param mixed $objects 100 | * 101 | * @throws MappingException 102 | * 103 | * @return array 104 | */ 105 | protected function normalizeAsArray($objects): array 106 | { 107 | $key = $this->relation; 108 | 109 | if (!is_array($objects) && !$objects instanceof Collection) { 110 | throw new MappingException("column '$key' should be of type array or collection"); 111 | } 112 | 113 | if ($objects instanceof Collection) { 114 | $objects = $objects->all(); 115 | } 116 | 117 | $normalizedObjects = []; 118 | 119 | foreach ($objects as $object) { 120 | $wrapper = $this->factory->make($object); 121 | $normalizedObjects[] = $wrapper->getEntityAttributes(); 122 | } 123 | 124 | if ($this->asJson) { 125 | $normalizedObjects = json_encode($normalizedObjects); 126 | } 127 | 128 | return [$key => $normalizedObjects]; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Relationships/Pivot.php: -------------------------------------------------------------------------------- 1 | setEntityAttributes($attributes); 86 | 87 | $this->table = $table; 88 | 89 | // We store off the parent instance so we will access the timestamp column names 90 | // for the model, since the pivot model timestamps aren't easily configurable 91 | // from the developer's point of view. We can use the parents to get these. 92 | $this->parent = $parent; 93 | 94 | $this->parentMap = $parentMap; 95 | 96 | $this->exists = $exists; 97 | 98 | $this->timestamps = $this->hasTimestampAttributes(); 99 | } 100 | 101 | /** 102 | * Get the foreign key column name. 103 | * 104 | * @return string 105 | */ 106 | public function getForeignKey() 107 | { 108 | return $this->foreignKey; 109 | } 110 | 111 | /** 112 | * Get the "other key" column name. 113 | * 114 | * @return string 115 | */ 116 | public function getOtherKey() 117 | { 118 | return $this->otherKey; 119 | } 120 | 121 | /** 122 | * Set the key names for the pivot model instance. 123 | * 124 | * @param string $foreignKey 125 | * @param string $otherKey 126 | * 127 | * @return $this 128 | */ 129 | public function setPivotKeys($foreignKey, $otherKey) 130 | { 131 | $this->foreignKey = $foreignKey; 132 | 133 | $this->otherKey = $otherKey; 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * Determine if the pivot model has timestamp attributes. 140 | * 141 | * @return bool 142 | */ 143 | public function hasTimestampAttributes() 144 | { 145 | return array_key_exists($this->getCreatedAtColumn(), $this->attributes); 146 | } 147 | 148 | /** 149 | * Get the name of the "created at" column. 150 | * 151 | * @return string 152 | */ 153 | public function getCreatedAtColumn() 154 | { 155 | return $this->parentMap->getCreatedAtColumn(); 156 | } 157 | 158 | /** 159 | * Get the name of the "updated at" column. 160 | * 161 | * @return string 162 | */ 163 | public function getUpdatedAtColumn() 164 | { 165 | return $this->parentMap->getUpdatedAtColumn(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Repository.php: -------------------------------------------------------------------------------- 1 | mapper = Manager::getMapper($mapper, $entityMap); 44 | } elseif ($mapper instanceof Mapper) { 45 | $this->mapper = $mapper; 46 | } else { 47 | new InvalidArgumentException('Repository class constructor need a valid Mapper or Mappable object.'); 48 | } 49 | } 50 | 51 | /** 52 | * Return all Entities from database. 53 | * 54 | * @return \Illuminate\Support\Collection 55 | */ 56 | public function all() 57 | { 58 | return $this->mapper->get(); 59 | } 60 | 61 | /** 62 | * Fetch a record from the database. 63 | * 64 | * @param int $id 65 | * 66 | * @return \Analogue\ORM\Mappable 67 | */ 68 | public function find($id) 69 | { 70 | return $this->mapper->find($id); 71 | } 72 | 73 | /** 74 | * Get the first entity matching the given attributes. 75 | * 76 | * @param array $attributes 77 | * 78 | * @return \Analogue\ORM\Mappable|null 79 | */ 80 | public function firstMatching(array $attributes) 81 | { 82 | return $this->mapper->where($attributes)->first(); 83 | } 84 | 85 | /** 86 | * Return all the entities matching the given attributes. 87 | * 88 | * @param array $attributes 89 | * 90 | * @return \Analogue\ORM\EntityCollection 91 | */ 92 | public function allMatching(array $attributes) 93 | { 94 | return $this->mapper->where($attributes)->get(); 95 | } 96 | 97 | /** 98 | * Return a paginator instance on the EntityCollection. 99 | * 100 | * @param int|null $perPage number of item per page (fallback on default setup in entity map) 101 | * 102 | * @return \Illuminate\Pagination\LengthAwarePaginator 103 | */ 104 | public function paginate($perPage = null) 105 | { 106 | return $this->mapper->paginate($perPage); 107 | } 108 | 109 | /** 110 | * Delete an entity or an entity collection from the database. 111 | * 112 | * @param Mappable|EntityCollection $entity 113 | * 114 | * @throws MappingException 115 | * @throws \InvalidArgumentException 116 | * 117 | * @return \Illuminate\Support\Collection|null 118 | */ 119 | public function delete($entity) 120 | { 121 | return $this->mapper->delete($entity); 122 | } 123 | 124 | /** 125 | * Persist an entity or an entity collection in the database. 126 | * 127 | * @param Mappable|EntityCollection|array $entity 128 | * 129 | * @throws MappingException 130 | * @throws \InvalidArgumentException 131 | * 132 | * @return Mappable|EntityCollection|array 133 | */ 134 | public function store($entity) 135 | { 136 | return $this->mapper->store($entity); 137 | } 138 | 139 | /** 140 | * Make custom mapper custom commands available in repository. 141 | * 142 | * @param string $method 143 | * @param array $parameters 144 | * 145 | * @throws Exception 146 | * 147 | * @return mixed 148 | */ 149 | public function __call($method, $parameters) 150 | { 151 | if ($this->mapper->hasCustomCommand($method)) { 152 | call_user_func_array([$this->mapper, $method], $parameters); 153 | } else { 154 | throw new Exception("No method $method on ".get_class($this)); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Relationships/MorphToMany.php: -------------------------------------------------------------------------------- 1 | inverse = $inverse; 50 | 51 | $this->morphType = $name.'_type'; 52 | 53 | $this->morphClass = $inverse ? $mapper->getEntityMap()->getClass() : get_class($parent); 54 | 55 | parent::__construct($mapper, $parent, $table, $foreignKey, $otherKey, $relationName); 56 | } 57 | 58 | /** 59 | * Set the where clause for the relation query. 60 | * 61 | * @return self 62 | */ 63 | protected function setWhere() 64 | { 65 | parent::setWhere(); 66 | 67 | $this->query->where($this->table.'.'.$this->morphType, $this->morphClass); 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Add the constraints for a relationship count query. 74 | * 75 | * @param Query $query 76 | * @param Query $parent 77 | * 78 | * @return Query 79 | */ 80 | public function getRelationCountQuery(Query $query, Query $parent) 81 | { 82 | $query = parent::getRelationCountQuery($query, $parent); 83 | 84 | return $query->where($this->table.'.'.$this->morphType, $this->morphClass); 85 | } 86 | 87 | /** 88 | * Set the constraints for an eager load of the relation. 89 | * 90 | * @param array $results 91 | * 92 | * @return void 93 | */ 94 | public function addEagerConstraints(array $results) 95 | { 96 | parent::addEagerConstraints($results); 97 | 98 | $this->query->where($this->table.'.'.$this->morphType, $this->morphClass); 99 | } 100 | 101 | /** 102 | * Create a new pivot attachment record. 103 | * 104 | * @param int $id 105 | * @param bool $timed 106 | * 107 | * @return array 108 | */ 109 | protected function createAttachRecord($id, $timed) 110 | { 111 | $record = parent::createAttachRecord($id, $timed); 112 | 113 | return array_add($record, $this->morphType, $this->morphClass); 114 | } 115 | 116 | /** 117 | * Create a new query builder for the pivot table. 118 | * 119 | * @throws \InvalidArgumentException 120 | * 121 | * @return \Illuminate\Database\Query\Builder 122 | */ 123 | protected function newPivotQuery() 124 | { 125 | $query = parent::newPivotQuery(); 126 | 127 | return $query->where($this->morphType, $this->morphClass); 128 | } 129 | 130 | /** 131 | * Create a new pivot model instance. 132 | * 133 | * @param array $attributes 134 | * @param bool $exists 135 | * 136 | * @return Pivot 137 | */ 138 | public function newPivot(array $attributes = [], $exists = false) 139 | { 140 | $pivot = new MorphPivot($this->parent, $this->parentMap, $attributes, $this->table, $exists); 141 | 142 | $pivot->setPivotKeys($this->foreignKey, $this->otherKey) 143 | ->setMorphType($this->morphType) 144 | ->setMorphClass($this->morphClass); 145 | 146 | return $pivot; 147 | } 148 | 149 | /** 150 | * Return Pivot attributes when available on a relationship. 151 | * 152 | * @return array 153 | */ 154 | public function getPivotAttributes() 155 | { 156 | return $this->pivotColumns; 157 | } 158 | 159 | /** 160 | * Get the foreign key "type" name. 161 | * 162 | * @return string 163 | */ 164 | public function getMorphType() 165 | { 166 | return $this->morphType; 167 | } 168 | 169 | /** 170 | * Get the class name of the parent model. 171 | * 172 | * @return string 173 | */ 174 | public function getMorphClass() 175 | { 176 | return $this->morphClass; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/ValueMap.php: -------------------------------------------------------------------------------- 1 | attributes; 58 | } 59 | 60 | /** 61 | * [getAttributesArrayName description]. 62 | * 63 | * @return [type] [description] 64 | */ 65 | public function getAttributesArrayName() 66 | { 67 | return $this->arrayName; 68 | } 69 | 70 | public function usesAttributesArray() 71 | { 72 | return $this->arrayName != null; 73 | } 74 | 75 | /** 76 | * @return array 77 | */ 78 | public function getProperties() 79 | { 80 | return $this->properties; 81 | } 82 | 83 | /** 84 | * @return array 85 | */ 86 | public function getEmbeddables() 87 | { 88 | return $this->embeddables; 89 | } 90 | 91 | /** 92 | * @param $class 93 | */ 94 | public function setClass($class) 95 | { 96 | $this->class = $class; 97 | } 98 | 99 | /** 100 | * @return mixed 101 | */ 102 | public function getClass() 103 | { 104 | return $this->class; 105 | } 106 | 107 | /** 108 | * @return string 109 | */ 110 | public function getName() 111 | { 112 | if (isset($this->name)) { 113 | return $this->name; 114 | } else { 115 | return class_basename($this); 116 | } 117 | } 118 | 119 | /** 120 | * Maps the names of the column names to the appropriate attributes 121 | * of an entity if the $attributes property of an EntityMap is an 122 | * associative array. 123 | * 124 | * @param array $array 125 | * 126 | * @return array 127 | */ 128 | public function getAttributeNamesFromColumns($array) 129 | { 130 | if (!empty($this->mappings)) { 131 | $newArray = []; 132 | foreach ($array as $key => $value) { 133 | $attributeName = isset($this->mappings[$key]) ? $this->mappings[$key] : $key; 134 | $newArray[$attributeName] = $value; 135 | } 136 | 137 | return $newArray; 138 | } 139 | 140 | return $array; 141 | } 142 | 143 | /** 144 | * Gets the entity attribute name of a given column in a table. 145 | * 146 | * @param string $columnName 147 | * 148 | * @return string 149 | */ 150 | public function getAttributeNameForColumn($columnName) 151 | { 152 | if (!empty($this->mappings)) { 153 | if (isset($this->mappings[$columnName])) { 154 | return $this->mappings[$columnName]; 155 | } 156 | } 157 | 158 | return $columnName; 159 | } 160 | 161 | /** 162 | * Gets the column name of a given entity attribute. 163 | * 164 | * @param string $attributeName 165 | * 166 | * @return string 167 | */ 168 | public function getColumnNameForAttribute($attributeName) 169 | { 170 | if (!empty($this->mappings)) { 171 | $flipped = array_flip($this->mappings); 172 | if (isset($flipped[$attributeName])) { 173 | return $flipped[$attributeName]; 174 | } 175 | } 176 | 177 | return $attributeName; 178 | } 179 | 180 | /** 181 | * Maps the attribute names of an entity to the appropriate 182 | * column names in the database if the $attributes property of 183 | * an EntityMap is an associative array. 184 | * 185 | * @param array $array 186 | * 187 | * @return array 188 | */ 189 | public function getColumnNamesFromAttributes($array) 190 | { 191 | if (!empty($this->mappings)) { 192 | $newArray = []; 193 | $flipped = array_flip($this->mappings); 194 | foreach ($array as $key => $value) { 195 | $attributeName = isset($flipped[$key]) ? $flipped[$key] : $key; 196 | $newArray[$attributeName] = $value; 197 | } 198 | 199 | return $newArray; 200 | } 201 | 202 | return $array; 203 | } 204 | 205 | public function hasAttribute($attribute) 206 | { 207 | if (!empty($this->mappings)) { 208 | return in_array($attribute, array_values($this->mappings)); 209 | } 210 | 211 | return in_array($attribute, $attributes); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/System/Builders/EntityBuilder.php: -------------------------------------------------------------------------------- 1 | mapper = $mapper; 62 | 63 | $this->entityMap = $mapper->getEntityMap(); 64 | 65 | $this->eagerLoads = $eagerLoads; 66 | 67 | $this->factory = new Factory(); 68 | 69 | $this->useCache = $useCache; 70 | } 71 | 72 | /** 73 | * Convert an array of attributes into an entity, or retrieve entity instance from cache. 74 | * 75 | * @param array $attributes 76 | * 77 | * @return mixed 78 | */ 79 | public function build(array $attributes) 80 | { 81 | // If the object we are building is a value object, 82 | // we won't be using the instance cache. 83 | if (!$this->useCache || $this->entityMap->getKeyName() === null) { 84 | return $this->buildEntity($attributes); 85 | } 86 | 87 | $instanceCache = $this->mapper->getInstanceCache(); 88 | 89 | $id = $this->getPrimaryKeyValue($attributes); 90 | 91 | return $instanceCache->has($id) ? $instanceCache->get($id) : $this->buildEntity($attributes); 92 | } 93 | 94 | /** 95 | * Actually build an entity. 96 | * 97 | * @param array $attributes 98 | * 99 | * @return mixed 100 | */ 101 | protected function buildEntity(array $attributes) 102 | { 103 | $wrapper = $this->getWrapperInstance(); 104 | 105 | // Hydrate any embedded Value Object 106 | // 107 | // TODO Move this to the result builder instead, 108 | // as we'll handle this the same way as they were 109 | // eager loaded relationships. 110 | $this->hydrateValueObjects($attributes); 111 | 112 | $wrapper->setEntityAttributes($attributes); 113 | 114 | $wrapper->setProxies(); 115 | 116 | $entity = $wrapper->unwrap(); 117 | 118 | // Once the object has been hydrated, we'll add 119 | // the instance to the instance cache. 120 | if ($this->entityMap->getKeyName() !== null) { 121 | $id = $this->getPrimaryKeyValue($attributes); 122 | $this->mapper->getInstanceCache()->add($entity, $id); 123 | } 124 | 125 | return $entity; 126 | } 127 | 128 | /** 129 | * Return the primary key value from attributes. 130 | * 131 | * @param array $attributes 132 | * 133 | * @return string 134 | */ 135 | protected function getPrimaryKeyValue(array $attributes) 136 | { 137 | return $attributes[$this->entityMap->getKeyName()]; 138 | } 139 | 140 | /** 141 | * Get the correct wrapper prototype corresponding to the object type. 142 | * 143 | * @throws \Analogue\ORM\Exceptions\MappingException 144 | * 145 | * @return InternallyMappable 146 | */ 147 | protected function getWrapperInstance() 148 | { 149 | return $this->factory->make($this->mapper->newInstance()); 150 | } 151 | 152 | /** 153 | * Hydrate value object embedded in this entity. 154 | * 155 | * @param array $attributes 156 | * 157 | * @throws \Analogue\ORM\Exceptions\MappingException 158 | * 159 | * @return void 160 | */ 161 | protected function hydrateValueObjects(&$attributes) 162 | { 163 | foreach ($this->entityMap->getEmbeddables() as $localKey => $valueClass) { 164 | $this->hydrateValueObject($attributes, $localKey, $valueClass); 165 | } 166 | } 167 | 168 | /** 169 | * Hydrate a single value object. 170 | * 171 | * @param array $attributes 172 | * @param string $localKey 173 | * @param string $valueClass 174 | * 175 | * @throws \Analogue\ORM\Exceptions\MappingException 176 | * 177 | * @return void 178 | */ 179 | protected function hydrateValueObject(&$attributes, $localKey, $valueClass) 180 | { 181 | $map = $this->mapper->getManager()->getValueMap($valueClass); 182 | 183 | $embeddedAttributes = $map->getAttributes(); 184 | 185 | $valueObject = $this->mapper->getManager()->getValueObjectInstance($valueClass); 186 | $voWrapper = $this->factory->make($valueObject); 187 | 188 | foreach ($embeddedAttributes as $key) { 189 | $prefix = snake_case(class_basename($valueClass)).'_'; 190 | 191 | $voWrapper->setEntityAttribute($key, $attributes[$prefix.$key]); 192 | 193 | unset($attributes[$prefix.$key]); 194 | } 195 | 196 | $attributes[$localKey] = $voWrapper->getObject(); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Relationships/EmbedsOne.php: -------------------------------------------------------------------------------- 1 | asArray ? $this->matchAsArray($attributes) : $this->matchAsAttributes($attributes); 40 | } 41 | 42 | /** 43 | * Match array attribute from parent to an embedded object, 44 | * and return the updated attributes. 45 | * 46 | * @param array $attributes 47 | * 48 | * @throws MappingException 49 | * 50 | * @return array 51 | */ 52 | protected function matchAsArray(array $attributes): array 53 | { 54 | // Extract the attributes with the key of the relation, 55 | // which should be an array. 56 | $key = $this->relation; 57 | 58 | if (!array_key_exists($key, $attributes) && !is_array($key)) { 59 | throw new MappingException("'$key' column should be an array"); 60 | } 61 | 62 | if ($this->asJson) { 63 | $attributes[$key] = json_decode($attributes[$key], true); 64 | } 65 | 66 | $attributes[$key] = $this->buildEmbeddedObject($attributes[$key]); 67 | 68 | return $attributes; 69 | } 70 | 71 | /** 72 | * Transform attributes from the parent entity result into 73 | * an embedded object, and return the updated attributes. 74 | * 75 | * @param array $attributes 76 | * 77 | * @return array 78 | */ 79 | protected function matchAsAttributes(array $attributes): array 80 | { 81 | $attributesMap = $this->getAttributesDictionnary(); 82 | 83 | // Get the subset that only the embedded object is concerned with and, convert it back 84 | // to embedded object attributes keys 85 | $originalAttributes = array_only($attributes, $attributesMap); 86 | 87 | $embeddedAttributes = []; 88 | 89 | foreach ($originalAttributes as $key => $value) { 90 | $embeddedKey = array_search($key, $attributesMap); 91 | $embeddedAttributes[$embeddedKey] = $value; 92 | } 93 | 94 | // Unset original attributes before, just in case one of the keys of the 95 | // original attributes is equals the relation name. 96 | foreach (array_keys($originalAttributes) as $key) { 97 | unset($attributes[$key]); 98 | } 99 | 100 | // Build object 101 | $attributes[$this->relation] = $this->buildEmbeddedObject($embeddedAttributes); 102 | 103 | return $attributes; 104 | } 105 | 106 | /** 107 | * Return a dictionary of attributes key on parent Entity. 108 | * 109 | * @return array 110 | */ 111 | protected function getAttributesDictionnary(): array 112 | { 113 | // Get attributes that belongs to the embedded object 114 | $embeddedAttributeKeys = $this->getEmbeddedObjectAttributes(); 115 | 116 | $attributesMap = []; 117 | 118 | // Build a dictionary for corresponding object attributes => parent attributes 119 | foreach ($embeddedAttributeKeys as $key) { 120 | $attributesMap[$key] = $this->getParentAttributeKey($key); 121 | } 122 | 123 | return $attributesMap; 124 | } 125 | 126 | /** 127 | * Transform embedded object into DB column(s). 128 | * 129 | * @param mixed $object 130 | * 131 | * @return array $columns 132 | */ 133 | public function normalize($object): array 134 | { 135 | return $this->asArray ? $this->normalizeAsArray($object) : $this->normalizeAsAttributes($object); 136 | } 137 | 138 | /** 139 | * Normalize object an array containing raw attributes. 140 | * 141 | * @param mixed $object 142 | * 143 | * @return array 144 | */ 145 | protected function normalizeAsArray($object): array 146 | { 147 | $wrapper = $this->factory->make($object); 148 | 149 | $value = $wrapper->getEntityAttributes(); 150 | 151 | if ($this->asJson) { 152 | $value = json_encode($value); 153 | } 154 | 155 | return [$this->relation => $value]; 156 | } 157 | 158 | /** 159 | * Normalize object as parent's attributes. 160 | * 161 | * @param mixed $object 162 | * 163 | * @return array 164 | */ 165 | protected function normalizeAsAttributes($object): array 166 | { 167 | if (is_null($object)) { 168 | return $this->nullObjectAttributes(); 169 | } 170 | 171 | $attributesMap = $this->getAttributesDictionnary(); 172 | 173 | $wrapper = $this->factory->make($object); 174 | 175 | $normalizedAttributes = []; 176 | 177 | foreach ($attributesMap as $embedKey => $parentKey) { 178 | $normalizedAttributes[$parentKey] = $wrapper->getEntityAttribute($embedKey); 179 | } 180 | 181 | return $normalizedAttributes; 182 | } 183 | 184 | /** 185 | * Set all object attributes to null. 186 | * 187 | * @return array 188 | */ 189 | protected function nullObjectAttributes(): array 190 | { 191 | $attributesMap = $this->getAttributesDictionnary(); 192 | 193 | $normalizedAttributes = []; 194 | 195 | foreach ($attributesMap as $embedKey => $parentKey) { 196 | $normalizedAttributes[$parentKey] = null; 197 | } 198 | 199 | return $normalizedAttributes; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Relationships/MorphTo.php: -------------------------------------------------------------------------------- 1 | morphType = $type; 52 | 53 | parent::__construct($mapper, $parent, $foreignKey, $otherKey, $relation); 54 | } 55 | 56 | /** 57 | * Set the constraints for an eager load of the relation. 58 | * 59 | * @param array $results 60 | * 61 | * @return void 62 | */ 63 | public function addEagerConstraints(array $results) 64 | { 65 | $this->buildDictionary($results); 66 | } 67 | 68 | /** 69 | * Build a dictionary with the entities. 70 | * 71 | * @param array $results 72 | * 73 | * @return void 74 | */ 75 | protected function buildDictionary($results) 76 | { 77 | foreach ($results as $result) { 78 | if ($result[$this->morphType]) { 79 | $this->dictionary[$result[$this->morphType]][$result[$this->foreignKey]][] = $result; 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Match the eagerly loaded results to their parents. 86 | * 87 | * @param array $results 88 | * @param string $relation 89 | * 90 | * @return array 91 | */ 92 | public function match(array $results, $relation) 93 | { 94 | foreach (array_keys($this->dictionary) as $type) { 95 | $results = $this->matchToMorphParents($type, $this->getResultsByType($type), $results); 96 | } 97 | 98 | return $results; 99 | } 100 | 101 | /** 102 | * Match the results for a given type to their parents. 103 | * 104 | * @param string $type 105 | * @param EntityCollection $results 106 | * @param array $parents 107 | * 108 | * @return array 109 | */ 110 | protected function matchToMorphParents($type, EntityCollection $results, array $parents) 111 | { 112 | $mapper = $this->relatedMapper->getManager()->mapper($type); 113 | $keyName = $mapper->getEntityMap()->getKeyName(); 114 | 115 | $keys = array_map(function ($parent) use ($keyName) { 116 | return $parent[$keyName]; 117 | }, $parents); 118 | 119 | $parents = array_combine($keys, $parents); 120 | 121 | foreach ($results as $result) { 122 | $key = $result[$keyName]; 123 | 124 | if (isset($this->dictionary[$type][$key])) { 125 | foreach ($this->dictionary[$type][$key] as $parent) { 126 | $parents[$parent[$keyName]][$this->relation] = $result; 127 | } 128 | } 129 | } 130 | 131 | return array_values($parents); 132 | } 133 | 134 | /** 135 | * Get all of the relation results for a type. 136 | * 137 | * @param string $type 138 | * 139 | * @throws \Analogue\ORM\Exceptions\MappingException 140 | * 141 | * @return \Illuminate\Support\Collection 142 | */ 143 | protected function getResultsByType($type) 144 | { 145 | $mapper = $this->relatedMapper->getManager()->mapper($type); 146 | 147 | $key = $mapper->getEntityMap()->getKeyName(); 148 | 149 | $query = $mapper->getQuery(); 150 | 151 | return $query->whereIn($key, array_keys($this->dictionary[$type]))->get(); 152 | } 153 | 154 | /** 155 | * Gather all of the foreign keys for a given type. 156 | * 157 | * @param string $type 158 | * 159 | * @return BaseCollection 160 | */ 161 | protected function gatherKeysByType($type) 162 | { 163 | $foreign = $this->foreignKey; 164 | 165 | return BaseCollection::make($this->dictionary[$type])->map(function ($entities) use ($foreign) { 166 | return head($entities)[$foreign]; 167 | })->unique(); 168 | } 169 | 170 | /** 171 | * Associate the model instance to the given parent. 172 | * 173 | * @param mixed $entity 174 | * 175 | * @return void 176 | */ 177 | public function associate($entity) 178 | { 179 | // The Mapper will retrieve this association within the object model, we won't be using 180 | // the foreign key attribute inside the parent Entity. 181 | // 182 | //$this->parent->setEntityAttribute($this->foreignKey, $entity->getEntityAttribute($this->otherKey)); 183 | // 184 | // Instead, we'll just add the object to the Entity's attribute 185 | 186 | $this->parent->setEntityAttribute($this->relation, $entity->getEntityObject()); 187 | } 188 | 189 | /** 190 | * Get the foreign key value pair for a related object. 191 | * 192 | * @param mixed $related 193 | * 194 | * @return array 195 | */ 196 | public function getForeignKeyValuePair($related) 197 | { 198 | $foreignKey = $this->getForeignKey(); 199 | 200 | if ($related) { 201 | $wrapper = $this->factory->make($related); 202 | 203 | $relatedKey = $this->relatedMap->getKeyName(); 204 | 205 | return [ 206 | $foreignKey => $wrapper->getEntityAttribute($relatedKey), 207 | $this->morphType => $wrapper->getMap()->getMorphClass(), 208 | ]; 209 | } else { 210 | return [$foreignKey => null]; 211 | } 212 | } 213 | 214 | /** 215 | * Get the dictionary used by the relationship. 216 | * 217 | * @return array 218 | */ 219 | public function getDictionary() 220 | { 221 | return $this->dictionary; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/Relationships/EmbeddedRelationship.php: -------------------------------------------------------------------------------- 1 | _". 51 | * 52 | * @var string 53 | */ 54 | protected $prefix; 55 | 56 | /** 57 | * Attributes Map allow the calling EntityMap to overrides attributes 58 | * on the embedded relation. 59 | * 60 | * @var array 61 | */ 62 | protected $columnMap = []; 63 | 64 | /** 65 | * Wrapper factory. 66 | * 67 | * @var \Analogue\ORM\System\Wrappers\Factory 68 | */ 69 | protected $factory; 70 | 71 | public function __construct($parent, string $relatedClass, string $relation) 72 | { 73 | $this->parentClass = get_class($parent); 74 | $this->relatedClass = $relatedClass; 75 | $this->relation = $relation; 76 | $this->prefix = $relation.'_'; 77 | $this->factory = new Factory(); 78 | } 79 | 80 | /** 81 | * Switch the 'store as array' feature. 82 | * 83 | * @param bool $storeAsArray 84 | * 85 | * @return static 86 | */ 87 | public function asArray(bool $storeAsArray = true) 88 | { 89 | $this->asArray = $storeAsArray; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Switch the 'store as json' feature. 96 | * 97 | * @param bool $storeAsJson 98 | * 99 | * @return static 100 | */ 101 | public function asJson(bool $storeAsJson = true) 102 | { 103 | $this->asJson = $storeAsJson; 104 | 105 | return $this->asArray(); 106 | } 107 | 108 | /** 109 | * Set the column map for the embedded relation. 110 | * 111 | * @param array $columns 112 | * 113 | * @return static 114 | */ 115 | public function setColumnMap(array $columns) 116 | { 117 | $this->columnMap = $columns; 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Set parent's attribute prefix. 124 | * 125 | * @param string $prefix 126 | * 127 | * @return static 128 | */ 129 | public function setPrefix(string $prefix) 130 | { 131 | $this->prefix = $prefix; 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * Return parent's attribute prefix. 138 | * 139 | * @return string 140 | */ 141 | public function getPrefix(): string 142 | { 143 | return $this->prefix; 144 | } 145 | 146 | /** 147 | * Get the embedded object's attributes that will be 148 | * hydrated using parent's entity attributes. 149 | * 150 | * @return array 151 | */ 152 | protected function getEmbeddedObjectAttributes(): array 153 | { 154 | $entityMap = $this->getRelatedMapper()->getEntityMap(); 155 | 156 | $attributes = $entityMap->getAttributes(); 157 | $properties = $entityMap->getProperties(); 158 | 159 | return array_merge($attributes, $properties); 160 | } 161 | 162 | /** 163 | * Get the corresponding attribute on parent's attributes. 164 | * 165 | * @param string $key 166 | * 167 | * @return string 168 | */ 169 | protected function getParentAttributeKey($key): string 170 | { 171 | return $this->getPrefixedAttributeKey($this->getMappedParentAttribute($key)); 172 | } 173 | 174 | /** 175 | * Get attribute name from the parent, if a map has been 176 | * defined. 177 | * 178 | * @param string $key 179 | * 180 | * @return string 181 | */ 182 | protected function getMappedParentAttribute(string $key): string 183 | { 184 | if (array_key_exists($key, $this->columnMap)) { 185 | return $this->columnMap[$key]; 186 | } else { 187 | return $key; 188 | } 189 | } 190 | 191 | /** 192 | * Return the name of the attribute with key. 193 | * 194 | * @param string $attributeKey 195 | * 196 | * @return string 197 | */ 198 | protected function getPrefixedAttributeKey(string $attributeKey): string 199 | { 200 | return $this->prefix.$attributeKey; 201 | } 202 | 203 | /** 204 | * Transform attributes into embedded object(s), and 205 | * match it into the given resultset. 206 | * 207 | * @return array 208 | */ 209 | abstract public function match(array $results): array; 210 | 211 | /** 212 | * Build an embedded object instance. 213 | * 214 | * @param array $attributes 215 | * 216 | * @return mixed 217 | */ 218 | protected function buildEmbeddedObject(array $attributes) 219 | { 220 | $resultBuilder = new ResultBuilder($this->getRelatedMapper(), true); 221 | 222 | // TODO : find a way to support eager load within an embedded 223 | // object. 224 | $eagerLoads = []; 225 | 226 | return $resultBuilder->build([$attributes], $eagerLoads)[0]; 227 | } 228 | 229 | /** 230 | * Transform embedded object into db column(s). 231 | * 232 | * @param mixed $object 233 | * 234 | * @return array $columns 235 | */ 236 | abstract public function normalize($object): array; 237 | 238 | /** 239 | * Return parent mapper. 240 | * 241 | * @return \Analogue\ORM\System\Mapper 242 | */ 243 | protected function getParentMapper() 244 | { 245 | return Manager::getInstance()->mapper($this->parentClass); 246 | } 247 | 248 | /** 249 | * Return embedded relationship mapper. 250 | * 251 | * @return \Analogue\ORM\System\Mapper 252 | */ 253 | protected function getRelatedMapper() 254 | { 255 | return Manager::getInstance()->mapper($this->relatedClass); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/Commands/Store.php: -------------------------------------------------------------------------------- 1 | aggregate->getEntityObject(); 23 | $wrappedEntity = $this->aggregate->getWrappedEntity(); 24 | $mapper = $this->aggregate->getMapper(); 25 | 26 | if ($mapper->fireEvent('storing', $wrappedEntity) === false) { 27 | return false; 28 | } 29 | 30 | $this->preStoreProcess(); 31 | 32 | /* 33 | * We will test the entity for existence 34 | * and run a creation if it doesn't exists 35 | */ 36 | if (!$this->aggregate->exists()) { 37 | if ($mapper->fireEvent('creating', $wrappedEntity) === false) { 38 | return false; 39 | } 40 | 41 | $this->insert(); 42 | 43 | $mapper->fireEvent('created', $wrappedEntity, false); 44 | } elseif ($this->aggregate->isDirty()) { 45 | if ($mapper->fireEvent('updating', $wrappedEntity) === false) { 46 | return false; 47 | } 48 | $this->update(); 49 | 50 | $mapper->fireEvent('updated', $wrappedEntity, false); 51 | } 52 | 53 | $this->postStoreProcess(); 54 | 55 | $mapper->fireEvent('stored', $wrappedEntity, false); 56 | 57 | // Once the object is stored, add it to the Instance cache 58 | $key = $this->aggregate->getEntityKeyValue(); 59 | 60 | if (!$mapper->getInstanceCache()->has($key)) { 61 | $mapper->getInstanceCache()->add($entity, $key); 62 | } 63 | 64 | $this->syncForeignKeyAttributes(); 65 | 66 | $wrappedEntity->unwrap(); 67 | 68 | return $entity; 69 | } 70 | 71 | /** 72 | * Run all operations that have to occur before actually 73 | * storing the entity. 74 | * 75 | * @throws \InvalidArgumentException 76 | * 77 | * @return void 78 | */ 79 | protected function preStoreProcess() 80 | { 81 | // Create any related object that doesn't exist in the database. 82 | $localRelationships = $this->aggregate->getEntityMap()->getLocalRelationships(); 83 | 84 | $this->createRelatedEntities($localRelationships); 85 | 86 | // Now we can sync the related collections 87 | $this->aggregate->syncRelationships($localRelationships); 88 | } 89 | 90 | /** 91 | * Check for existence and create non-existing related entities. 92 | * 93 | * @param array 94 | * 95 | * @throws \InvalidArgumentException 96 | * 97 | * @return void 98 | */ 99 | protected function createRelatedEntities($relations) 100 | { 101 | $entitiesToCreate = $this->aggregate->getNonExistingRelated($relations); 102 | 103 | foreach ($entitiesToCreate as $aggregate) { 104 | $this->createStoreCommand($aggregate)->execute(); 105 | } 106 | } 107 | 108 | /** 109 | * Create a new store command. 110 | * 111 | * @param Aggregate $aggregate 112 | * 113 | * @return Store 114 | */ 115 | protected function createStoreCommand(Aggregate $aggregate): self 116 | { 117 | // We gotta retrieve the corresponding query adapter to use. 118 | $mapper = $aggregate->getMapper(); 119 | 120 | return new self($aggregate, $mapper->newQueryBuilder()); 121 | } 122 | 123 | /** 124 | * Run all operations that have to occur after the entity 125 | * is stored. 126 | * 127 | * @throws \InvalidArgumentException 128 | * 129 | * @return void 130 | */ 131 | protected function postStoreProcess() 132 | { 133 | $aggregate = $this->aggregate; 134 | 135 | // Create any related object that doesn't exist in the database. 136 | $foreignRelationships = $aggregate->getEntityMap()->getForeignRelationships(); 137 | 138 | $this->createRelatedEntities($foreignRelationships); 139 | 140 | // Update any pivot tables that has been modified. 141 | $aggregate->updatePivotRecords(); 142 | 143 | // Update any dirty relationship. This include relationships that already exists, have 144 | // dirty attributes / newly created related entities / dirty related entities. 145 | $dirtyRelatedAggregates = $aggregate->getDirtyRelationships(); 146 | 147 | foreach ($dirtyRelatedAggregates as $related) { 148 | $this->createStoreCommand($related)->execute(); 149 | } 150 | 151 | // Now we can sync the related collections 152 | // 153 | // TODO (note) : not sure this check is needed, as we can assume 154 | // the aggregate exists in the Post Store Process 155 | if ($this->aggregate->exists()) { 156 | $this->aggregate->syncRelationships($foreignRelationships); 157 | } 158 | 159 | // TODO be move it to the wrapper class 160 | // so it's the same code for the entity builder 161 | $aggregate->setProxies(); 162 | 163 | // Update Entity Cache 164 | $aggregate->getMapper()->getEntityCache()->refresh($aggregate); 165 | } 166 | 167 | /** 168 | * Execute an insert statement on the database. 169 | * 170 | * @return void 171 | */ 172 | protected function insert() 173 | { 174 | $aggregate = $this->aggregate; 175 | 176 | $attributes = $aggregate->getRawAttributes(); 177 | 178 | $keyName = $aggregate->getEntityMap()->getKeyName(); 179 | 180 | // Check if the primary key is defined in the attributes 181 | if (array_key_exists($keyName, $attributes) && $attributes[$keyName] != null) { 182 | $this->query->insert($attributes); 183 | } else { 184 | // Prevent inserting with a null ID 185 | if (array_key_exists($keyName, $attributes)) { 186 | unset($attributes[$keyName]); 187 | } 188 | 189 | if (isset($attributes['attributes'])) { 190 | unset($attributes['attributes']); 191 | } 192 | 193 | $id = $this->query->insertGetId($attributes, $keyName); 194 | 195 | $aggregate->setEntityAttribute($keyName, $id); 196 | } 197 | } 198 | 199 | /** 200 | * Update attributes on actual entity. 201 | * 202 | * @param array $attributes 203 | * 204 | * @return void 205 | */ 206 | protected function syncForeignKeyAttributes() 207 | { 208 | $attributes = $this->aggregate->getForeignKeyAttributes(); 209 | 210 | foreach ($attributes as $key => $value) { 211 | $this->aggregate->setEntityAttribute($key, $value); 212 | } 213 | } 214 | 215 | /** 216 | * Run an update statement on the entity. 217 | * 218 | * @throws \InvalidArgumentException 219 | * 220 | * @return void 221 | */ 222 | protected function update() 223 | { 224 | $key = $this->aggregate->getEntityKeyName(); 225 | $value = $this->aggregate->getEntityKeyValue(); 226 | 227 | $this->query->where($key, $value); 228 | 229 | $dirtyAttributes = $this->aggregate->getDirtyRawAttributes(); 230 | 231 | if (count($dirtyAttributes) > 0) { 232 | $this->query->update($dirtyAttributes); 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/Relationships/HasOneOrMany.php: -------------------------------------------------------------------------------- 1 | localKey = $localKey; 35 | $this->foreignKey = $foreignKey; 36 | 37 | parent::__construct($mapper, $parentEntity); 38 | } 39 | 40 | /** 41 | * @param \Analogue\ORM\Entity $entity 42 | */ 43 | public function attachOne($entity) 44 | { 45 | $wrapper = $this->factory->make($entity); 46 | 47 | // Ok, we need to guess the inverse of the relation from there. 48 | // Let's assume the inverse of the relation method is the name of 49 | // the entity. 50 | 51 | $wrapper->setEntityAttribute($this->getPlainForeignKey(), $this->getParentKey()); 52 | } 53 | 54 | /** 55 | * @param EntityCollection $entities 56 | */ 57 | public function attachMany(EntityCollection $entities) 58 | { 59 | foreach ($entities as $entity) { 60 | $this->attachOne($entity); 61 | } 62 | } 63 | 64 | /** 65 | * @param $entityHash 66 | */ 67 | protected function detachOne($entityHash) 68 | { 69 | $this->detachMany([$entityHash]); 70 | } 71 | 72 | /** 73 | * Attach ids that are passed as arguments, and detach any other. 74 | * 75 | * @param mixed $entities 76 | * 77 | * @throws \InvalidArgumentException 78 | * 79 | * @return void 80 | */ 81 | public function sync(array $entities) 82 | { 83 | $this->detachExcept($entities); 84 | } 85 | 86 | /** 87 | * @param $entities 88 | * 89 | * @throws \InvalidArgumentException 90 | */ 91 | protected function detachExcept($entities) 92 | { 93 | $query = $this->query->getQuery()->from($this->relatedMap->getTable()); 94 | 95 | if (count($entities) > 0) { 96 | $keys = $this->getKeys($entities); 97 | $query->whereNotIn($this->relatedMap->getKeyName(), $keys); 98 | } 99 | 100 | $parentKey = $this->parentMap->getKeyName(); 101 | 102 | $query->where($this->getPlainForeignKey(), '=', $this->parent->getEntityAttribute($parentKey)) 103 | ->update([$this->getPlainForeignKey() => null]); 104 | } 105 | 106 | /** 107 | * @param array $entityHashes 108 | */ 109 | public function detachMany(array $entityHashes) 110 | { 111 | $keys = []; 112 | 113 | foreach ($entityHashes as $hash) { 114 | $split = explode('.', $hash); 115 | $keys[] = $split[1]; 116 | } 117 | 118 | $query = $this->query->getQuery()->from($this->relatedMap->getTable()); 119 | 120 | $query->whereIn($this->relatedMap->getKeyName(), $keys) 121 | ->update([$this->getPlainForeignKey() => null]); 122 | } 123 | 124 | /** 125 | * Set the base constraints on the relation query. 126 | * 127 | * @return void 128 | */ 129 | public function addConstraints() 130 | { 131 | if (static::$constraints) { 132 | $this->query->where($this->foreignKey, '=', $this->getParentKey()); 133 | } 134 | } 135 | 136 | /** 137 | * Set the constraints for an eager load of the relation. 138 | * 139 | * @param array $results 140 | * 141 | * @return void 142 | */ 143 | public function addEagerConstraints(array $results) 144 | { 145 | $this->query->whereIn($this->foreignKey, $this->getKeysFromResults($results, $this->localKey)); 146 | } 147 | 148 | /** 149 | * Match the eagerly loaded relationship to the current result set. 150 | * 151 | * @param array $results 152 | * @param string $relation 153 | * 154 | * @return array 155 | */ 156 | public function matchOne(array $results, $relation) 157 | { 158 | return $this->matchOneOrMany($results, $relation, 'one'); 159 | } 160 | 161 | /** 162 | * Match the eagerly loaded results to their many parents. 163 | * 164 | * @param array $results 165 | * @param string $relation 166 | * 167 | * @return array 168 | */ 169 | public function matchMany(array $results, $relation) 170 | { 171 | return $this->matchOneOrMany($results, $relation, 'many'); 172 | } 173 | 174 | /** 175 | * Match the eagerly loaded results to their many parents. 176 | * 177 | * @param array $results 178 | * @param string $relation 179 | * @param string $type 180 | * 181 | * @return array 182 | */ 183 | protected function matchOneOrMany(array $results, $relation, $type) 184 | { 185 | $entities = $this->getEager(); 186 | 187 | $dictionary = $this->buildDictionary($entities); 188 | 189 | $cache = $this->parentMapper->getEntityCache(); 190 | 191 | $host = $this; 192 | 193 | // Once we have the dictionary we can simply spin through the parent models to 194 | // link them up with their children using the keyed dictionary to make the 195 | // matching very convenient and easy work. Then we'll just return them. 196 | return array_map(function ($result) use ($dictionary, $cache, $type, $relation, $host) { 197 | $key = $result[$host->localKey]; 198 | 199 | if (isset($dictionary[$key])) { 200 | $value = $host->getRelationValue($dictionary, $key, $type); 201 | 202 | $result[$relation] = $value; 203 | 204 | // TODO : Refactor This 205 | $cache->cacheLoadedRelationResult($key, $relation, $value, $this); 206 | } else { 207 | $result[$relation] = $type === 'many' ? $this->relatedMap->newCollection() : null; 208 | } 209 | 210 | return $result; 211 | }, $results); 212 | } 213 | 214 | /** 215 | * Get the value of a relationship by one or many type. 216 | * 217 | * @param array $dictionary 218 | * @param string $key 219 | * @param string $type 220 | * 221 | * @return mixed 222 | */ 223 | protected function getRelationValue(array $dictionary, $key, $type) 224 | { 225 | $value = $dictionary[$key]; 226 | 227 | return $type == 'one' ? reset($value) : $this->relatedMap->newCollection($value); 228 | } 229 | 230 | /** 231 | * Build model dictionary keyed by the relation's foreign key. 232 | * 233 | * @param EntityCollection $results 234 | * 235 | * @return array 236 | */ 237 | protected function buildDictionary(EntityCollection $results) 238 | { 239 | $dictionary = []; 240 | 241 | $foreign = $this->getPlainForeignKey(); 242 | 243 | // First we will create a dictionary of models keyed by the foreign key of the 244 | // relationship as this will allow us to quickly access all of the related 245 | // models without having to do nested looping which will be quite slow. 246 | foreach ($results as $result) { 247 | $dictionary[$result->{$foreign}][] = $result; 248 | } 249 | 250 | return $dictionary; 251 | } 252 | 253 | /** 254 | * Get the key for comparing against the parent key in "has" query. 255 | * 256 | * @return string 257 | */ 258 | public function getHasCompareKey() 259 | { 260 | return $this->getForeignKey(); 261 | } 262 | 263 | /** 264 | * Get the foreign key for the relationship. 265 | * 266 | * @return string 267 | */ 268 | public function getForeignKey() 269 | { 270 | return $this->foreignKey; 271 | } 272 | 273 | /** 274 | * Get the plain foreign key. 275 | * 276 | * @return string 277 | */ 278 | public function getPlainForeignKey() 279 | { 280 | $segments = explode('.', $this->getForeignKey()); 281 | 282 | return $segments[count($segments) - 1]; 283 | } 284 | 285 | /** 286 | * Get the key value of the parent's local key. 287 | * 288 | * @return mixed 289 | */ 290 | public function getParentKey() 291 | { 292 | return $this->parent->getEntityAttribute($this->localKey); 293 | } 294 | 295 | /** 296 | * Get the fully qualified parent key name. 297 | * 298 | * @return string 299 | */ 300 | public function getQualifiedParentKeyName() 301 | { 302 | return $this->parentMap->getTable().'.'.$this->localKey; 303 | } 304 | 305 | /** 306 | * Get the foreign key as value pair for this relation. 307 | * 308 | * @return array 309 | */ 310 | public function getForeignKeyValuePair() 311 | { 312 | return [$this->getPlainForeignKey() => $this->getParentKey()]; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/Relationships/HasManyThrough.php: -------------------------------------------------------------------------------- 1 | firstKey = $firstKey; 55 | $this->secondKey = $secondKey; 56 | $this->farParent = $farParent; 57 | 58 | $this->farParentMap = $mapper->getManager()->mapper($farParent)->getEntityMap(); 59 | $parentInstance = $mapper->getManager()->mapper($parentMap->getClass())->newInstance(); 60 | 61 | parent::__construct($mapper, $parentInstance); 62 | } 63 | 64 | /** 65 | * Set the base constraints on the relation query. 66 | * 67 | * @return void 68 | */ 69 | public function addConstraints() 70 | { 71 | $parentTable = $this->parentMap->getTable(); 72 | 73 | $this->setJoin(); 74 | 75 | if (static::$constraints) { 76 | $farParentKeyName = $this->farParentMap->getKeyName(); 77 | 78 | $this->query->where( 79 | $parentTable.'.'.$this->firstKey, 80 | '=', 81 | $this->farParent->getEntityAttribute($farParentKeyName) 82 | ); 83 | } 84 | } 85 | 86 | /** 87 | * Add the constraints for a relationship count query. 88 | * 89 | * @param Query $query 90 | * @param Query $parent 91 | * 92 | * @return Query 93 | */ 94 | public function getRelationCountQuery(Query $query, Query $parent) 95 | { 96 | $parentTable = $this->parentMap->getTable(); 97 | 98 | $this->setJoin($query); 99 | 100 | $query->select(new Expression('count(*)')); 101 | 102 | $key = $this->wrap($parentTable.'.'.$this->firstKey); 103 | 104 | return $query->where($this->getHasCompareKey(), '=', new Expression($key)); 105 | } 106 | 107 | /** 108 | * Set the join clause on the query. 109 | * 110 | * @param null|Query $query 111 | * 112 | * @return void 113 | */ 114 | protected function setJoin(Query $query = null) 115 | { 116 | $query = $query ?: $this->query; 117 | 118 | $foreignKey = $this->relatedMap->getTable().'.'.$this->secondKey; 119 | 120 | $query->join($this->parentMap->getTable(), $this->getQualifiedParentKeyName(), '=', $foreignKey); 121 | } 122 | 123 | /** 124 | * Set the constraints for an eager load of the relation. 125 | * 126 | * @param array $results 127 | * 128 | * @return void 129 | */ 130 | public function addEagerConstraints(array $results) 131 | { 132 | $table = $this->parentMap->getTable(); 133 | 134 | $this->query->whereIn($table.'.'.$this->firstKey, $this->getKeysFromResults($results)); 135 | } 136 | 137 | /** 138 | * Match eagerly loaded relationship to a result set. 139 | * 140 | * @param array $results 141 | * @param string $relation 142 | * 143 | * @return array 144 | */ 145 | public function match(array $results, $relation) 146 | { 147 | $entities = $this->getEager(); 148 | 149 | $dictionary = $this->buildDictionary($entities); 150 | 151 | $relatedKey = $this->relatedMap->getKeyName(); 152 | 153 | $cache = $this->parentMapper->getEntityCache(); 154 | 155 | $host = $this; 156 | 157 | // Once we have the dictionary we can simply spin through the parent entities to 158 | // link them up with their children using the keyed dictionary to make the 159 | // matching very convenient and easy work. Then we'll just return them. 160 | return array_map(function ($result) use ($relation, $relatedKey, $dictionary, $cache, $host) { 161 | $key = $result[$relatedKey]; 162 | 163 | if (isset($dictionary[$key])) { 164 | $value = $host->relatedMap->newCollection($dictionary[$key]); 165 | 166 | $result[$relation] = $value; 167 | 168 | $cache->cacheLoadedRelationResult($key, $relation, $value, $this); 169 | } 170 | 171 | return $result; 172 | }, $results); 173 | } 174 | 175 | /** 176 | * Build model dictionary keyed by the relation's foreign key. 177 | * 178 | * @param EntityCollection $results 179 | * 180 | * @return array 181 | */ 182 | protected function buildDictionary(EntityCollection $results) 183 | { 184 | $dictionary = []; 185 | 186 | $foreign = $this->firstKey; 187 | 188 | // First we will create a dictionary of entities keyed by the foreign key of the 189 | // relationship as this will allow us to quickly access all of the related 190 | // entities without having to do nested looping which will be quite slow. 191 | foreach ($results as $result) { 192 | $dictionary[$result->{$foreign}][] = $result; 193 | } 194 | 195 | return $dictionary; 196 | } 197 | 198 | /** 199 | * Get the results of the relationship. 200 | * 201 | * @param $relation 202 | * 203 | * @return EntityCollection 204 | */ 205 | public function getResults($relation) 206 | { 207 | $results = $this->query->get(); 208 | 209 | $this->cacheRelation($results, $relation); 210 | 211 | return $results; 212 | } 213 | 214 | /** 215 | * Execute the query as a "select" statement. 216 | * 217 | * @param array $columns 218 | * 219 | * @return EntityCollection 220 | */ 221 | public function get($columns = ['*']): Collection 222 | { 223 | // First we'll add the proper select columns onto the query so it is run with 224 | // the proper columns. Then, we will get the results and hydrate out pivot 225 | // entities with the result of those columns as a separate model relation. 226 | $select = $this->getSelectColumns($columns); 227 | 228 | $entities = $this->query->addSelect($select)->getEntities(); 229 | 230 | // If we actually found entities we will also eager load any relationships that 231 | // have been specified as needing to be eager loaded. This will solve the 232 | // n + 1 query problem for the developer and also increase performance. 233 | if (count($entities) > 0) { 234 | $entities = $this->query->eagerLoadRelations($entities); 235 | } 236 | 237 | return $this->relatedMap->newCollection($entities); 238 | } 239 | 240 | /** 241 | * Set the select clause for the relation query. 242 | * 243 | * @param array $columns 244 | * 245 | * @return BelongsToMany 246 | */ 247 | protected function getSelectColumns(array $columns = ['*']) 248 | { 249 | if ($columns == ['*']) { 250 | $columns = [$this->relatedMap->getTable().'.*']; 251 | } 252 | 253 | return array_merge($columns, [$this->parentMap->getTable().'.'.$this->firstKey]); 254 | } 255 | 256 | /** 257 | * Get a paginator for the "select" statement. 258 | * 259 | * @param int $perPage 260 | * @param array $columns 261 | * 262 | * @return \Illuminate\Pagination\LengthAwarePaginator 263 | */ 264 | public function paginate($perPage = null, $columns = ['*']) 265 | { 266 | $this->query->addSelect($this->getSelectColumns($columns)); 267 | 268 | return $this->query->paginate($perPage, $columns); 269 | } 270 | 271 | /** 272 | * Get the key name of the parent model. 273 | * 274 | * @return string 275 | */ 276 | protected function getQualifiedParentKeyName() 277 | { 278 | return $this->parentMap->getQualifiedKeyName(); 279 | } 280 | 281 | /** 282 | * Get the key for comparing against the parent key in "has" query. 283 | * 284 | * @return string 285 | */ 286 | public function getHasCompareKey() 287 | { 288 | return $this->farParentMap->getQualifiedKeyName(); 289 | } 290 | 291 | /** 292 | * Run synchronization content if needed by the 293 | * relation type. 294 | * 295 | * @param array $entities 296 | * 297 | * @return void 298 | */ 299 | public function sync(array $entities) 300 | { 301 | // N/A 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/Relationships/BelongsTo.php: -------------------------------------------------------------------------------- 1 | otherKey = $otherKey; 46 | $this->relation = $relation; 47 | $this->foreignKey = $foreignKey; 48 | 49 | parent::__construct($mapper, $parent); 50 | } 51 | 52 | /** 53 | * Get the results of the relationship. 54 | * 55 | * @param $relation 56 | * 57 | * @return \Analogue\ORM\Entity 58 | */ 59 | public function getResults($relation) 60 | { 61 | $result = $this->query->first(); 62 | 63 | $this->cacheRelation($result, $relation); 64 | 65 | return $result; 66 | } 67 | 68 | /** 69 | * Set the base constraints on the relation query. 70 | * 71 | * @return void 72 | */ 73 | public function addConstraints() 74 | { 75 | if (static::$constraints) { 76 | // For belongs to relationships, which are essentially the inverse of has one 77 | // or has many relationships, we need to actually query on the primary key 78 | // of the related models matching on the foreign key that's on a parent. 79 | $this->query->where($this->otherKey, '=', $this->parent->getEntityAttribute($this->foreignKey)); 80 | } 81 | } 82 | 83 | /** 84 | * Add the constraints for a relationship count query. 85 | * 86 | * @param Query $query 87 | * @param Query $parent 88 | * 89 | * @return Query 90 | */ 91 | public function getRelationCountQuery(Query $query, Query $parent) 92 | { 93 | $query->select(new Expression('count(*)')); 94 | 95 | $otherKey = $this->wrap($query->getTable().'.'.$this->otherKey); 96 | 97 | return $query->where($this->getQualifiedForeignKey(), '=', new Expression($otherKey)); 98 | } 99 | 100 | /** 101 | * Set the constraints for an eager load of the relation. 102 | * 103 | * @param array $results 104 | * 105 | * @return void 106 | */ 107 | public function addEagerConstraints(array $results) 108 | { 109 | // We'll grab the primary key name of the related models since it could be set to 110 | // a non-standard name and not "id". We will then construct the constraint for 111 | // our eagerly loading query so it returns the proper models from execution. 112 | $key = $this->otherKey; 113 | 114 | $this->query->whereIn($key, $this->getEagerModelKeys($results)); 115 | } 116 | 117 | /** 118 | * Gather the keys from an array of related models. 119 | * 120 | * @param array $results 121 | * 122 | * @return array 123 | */ 124 | protected function getEagerModelKeys(array $results) 125 | { 126 | $keys = []; 127 | 128 | // First we need to gather all of the keys from the result set so we know what 129 | // to query for via the eager loading query. We will add them to an array then 130 | // execute a "where in" statement to gather up all of those related records. 131 | foreach ($results as $result) { 132 | if (array_key_exists($this->foreignKey, $result) && !is_null($value = $result[$this->foreignKey])) { 133 | $keys[] = $value; 134 | } 135 | } 136 | 137 | // If there are no keys that were not null we will just return an array with 0 in 138 | // it so the query doesn't fail, but will not return any results, which should 139 | // be what this developer is expecting in a case where this happens to them. 140 | if (count($keys) == 0) { 141 | return [0]; 142 | } 143 | 144 | return array_values(array_unique($keys)); 145 | } 146 | 147 | /** 148 | * Match the Results array to an eagerly loaded relation. 149 | * 150 | * @param array $results 151 | * @param string $relation 152 | * 153 | * @return array 154 | */ 155 | public function match(array $results, $relation) 156 | { 157 | $foreign = $this->foreignKey; 158 | 159 | $other = $this->otherKey; 160 | 161 | // Execute the relationship and get related entities as an EntityCollection 162 | $entities = $this->getEager(); 163 | 164 | // First we will get to build a dictionary of the child models by their primary 165 | // key of the relationship, then we can easily match the children back onto 166 | // the parents using that dictionary and the primary key of the children. 167 | $dictionary = []; 168 | 169 | // TODO ; see if otherKey is the primary key of the related entity, we can 170 | // simply use the EntityCollection key to match entities to results, which 171 | // will be much more efficient, and use this method as a fallback if the 172 | // otherKey is not the same as the primary Key. 173 | foreach ($entities as $entity) { 174 | $entity = $this->factory->make($entity); 175 | $dictionary[$entity->getEntityAttribute($other)] = $entity->getObject(); 176 | } 177 | 178 | // Once we have the dictionary constructed, we can loop through all the parents 179 | // and match back onto their children using these keys of the dictionary and 180 | // the primary key of the children to map them onto the correct instances. 181 | return array_map(function ($result) use ($dictionary, $foreign, $relation) { 182 | if (array_key_exists($foreign, $result) && isset($dictionary[$result[$foreign]])) { 183 | $result[$relation] = $dictionary[$result[$foreign]]; 184 | } else { 185 | $result[$relation] = null; 186 | } 187 | 188 | return $result; 189 | }, $results); 190 | } 191 | 192 | public function sync(array $entities) 193 | { 194 | if (count($entities) > 1) { 195 | throw new MappingException("Single Relationship shouldn't be synced with more than one entity"); 196 | } 197 | 198 | if (count($entities) == 1) { 199 | return $this->associate($entities[0]); 200 | } 201 | 202 | return false; 203 | } 204 | 205 | /** 206 | * Associate the model instance to the given parent. 207 | * 208 | * @param mixed $entity 209 | * 210 | * @return void 211 | */ 212 | public function associate($entity) 213 | { 214 | $this->parent->setEntityAttribute($this->foreignKey, $entity->getEntityAttribute($this->otherKey)); 215 | } 216 | 217 | /** 218 | * Dissociate previously associated model from the given parent. 219 | * 220 | * @return Mappable 221 | */ 222 | public function dissociate() 223 | { 224 | // The Mapper will retrieve this association within the object model, we won't be using 225 | // the foreign key attribute inside the parent Entity. 226 | // 227 | //$this->parent->setEntityAttribute($this->foreignKey, null); 228 | 229 | $this->parent->setEntityAttribute($this->relation, null); 230 | } 231 | 232 | /** 233 | * Get the foreign key of the relationship. 234 | * 235 | * @return string 236 | */ 237 | public function getForeignKey() 238 | { 239 | return $this->foreignKey; 240 | } 241 | 242 | /** 243 | * Get the foreign key value pair for a related object. 244 | * 245 | * @param mixed $related 246 | * 247 | * @return array 248 | */ 249 | public function getForeignKeyValuePair($related) 250 | { 251 | $foreignKey = $this->getForeignKey(); 252 | 253 | if ($related) { 254 | $wrapper = $this->factory->make($related); 255 | 256 | $relatedKey = $this->relatedMap->getKeyName(); 257 | 258 | return [$foreignKey => $wrapper->getEntityAttribute($relatedKey)]; 259 | } else { 260 | return [$foreignKey => null]; 261 | } 262 | } 263 | 264 | /** 265 | * Get the fully qualified foreign key of the relationship. 266 | * 267 | * @return string 268 | */ 269 | public function getQualifiedForeignKey() 270 | { 271 | return $this->parentMap->getTable().'.'.$this->foreignKey; 272 | } 273 | 274 | /** 275 | * Get the associated key of the relationship. 276 | * 277 | * @return string 278 | */ 279 | public function getOtherKey() 280 | { 281 | return $this->otherKey; 282 | } 283 | 284 | /** 285 | * Get the fully qualified associated key of the relationship. 286 | * 287 | * @return string 288 | */ 289 | public function getQualifiedOtherKeyName() 290 | { 291 | return $this->relatedMap->getTable().'.'.$this->otherKey; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/EntityCollection.php: -------------------------------------------------------------------------------- 1 | factory = new Factory(); 28 | 29 | parent::__construct($entities); 30 | } 31 | 32 | /** 33 | * Find an entity in the collection by key. 34 | * 35 | * @param mixed $key 36 | * @param mixed $default 37 | * 38 | * @throws MappingException 39 | * 40 | * @return \Analogue\ORM\Entity 41 | */ 42 | public function find($key, $default = null) 43 | { 44 | if ($key instanceof Mappable) { 45 | $key = $this->getEntityKey($key); 46 | } 47 | 48 | return array_first($this->items, function ($entity, $itemKey) use ($key) { 49 | return $this->getEntityKey($entity) == $key; 50 | }, $default); 51 | } 52 | 53 | /** 54 | * Add an entity to the collection. 55 | * 56 | * @param Mappable $entity 57 | * 58 | * @return $this 59 | */ 60 | public function add($entity) 61 | { 62 | $this->push($entity); 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Remove an entity from the collection. 69 | * 70 | * @param $entity 71 | * 72 | * @throws MappingException 73 | * 74 | * @return mixed 75 | */ 76 | public function remove($entity) 77 | { 78 | $key = $this->getEntityKey($entity); 79 | 80 | return $this->pull($key); 81 | } 82 | 83 | /** 84 | * Get and remove an item from the collection. 85 | * 86 | * @param mixed $key 87 | * @param mixed $default 88 | * 89 | * @return mixed 90 | */ 91 | public function pull($key, $default = null) 92 | { 93 | $this->items = array_filter($this->items, function ($item) use ($key) { 94 | $primaryKey = $this->getEntityKey($item); 95 | 96 | return $primaryKey !== $key; 97 | }); 98 | } 99 | 100 | /** 101 | * Push an item onto the end of the collection. 102 | * 103 | * @param mixed $value 104 | * 105 | * @return void 106 | */ 107 | public function push($value) 108 | { 109 | $this->offsetSet(null, $value); 110 | } 111 | 112 | /** 113 | * Put an item in the collection by key. 114 | * 115 | * @param mixed $key 116 | * @param mixed $value 117 | * 118 | * @return void 119 | */ 120 | public function put($key, $value) 121 | { 122 | $this->offsetSet($key, $value); 123 | } 124 | 125 | /** 126 | * Set the item at a given offset. 127 | * 128 | * @param mixed $key 129 | * @param mixed $value 130 | * 131 | * @return void 132 | */ 133 | public function offsetSet($key, $value) 134 | { 135 | if (is_null($key)) { 136 | $this->items[] = $value; 137 | } else { 138 | $this->items[$key] = $value; 139 | } 140 | } 141 | 142 | /** 143 | * Fetch a nested element of the collection. 144 | * 145 | * @param string $key 146 | * 147 | * @return self 148 | */ 149 | public function fetch($key) 150 | { 151 | return new static(array_fetch($this->toArray(), $key)); 152 | } 153 | 154 | /** 155 | * Generic function for returning class.key value pairs. 156 | * 157 | * @throws MappingException 158 | * 159 | * @return string 160 | */ 161 | public function getEntityHashes() 162 | { 163 | return array_map(function ($entity) { 164 | $class = get_class($entity); 165 | 166 | $mapper = Manager::getMapper($class); 167 | 168 | $keyName = $mapper->getEntityMap()->getKeyName(); 169 | 170 | return $class.'.'.$entity->getEntityAttribute($keyName); 171 | }, 172 | $this->items); 173 | } 174 | 175 | /** 176 | * Get a subset of the collection from entity hashes. 177 | * 178 | * @param array $hashes 179 | * 180 | * @throws MappingException 181 | * 182 | * @return array 183 | */ 184 | public function getSubsetByHashes(array $hashes) 185 | { 186 | $subset = []; 187 | 188 | foreach ($this->items as $item) { 189 | $class = get_class($item); 190 | 191 | $mapper = Manager::getMapper($class); 192 | 193 | $keyName = $mapper->getEntityMap()->getKeyName(); 194 | 195 | if (in_array($class.'.'.$item->$keyName, $hashes)) { 196 | $subset[] = $item; 197 | } 198 | } 199 | 200 | return $subset; 201 | } 202 | 203 | /** 204 | * Merge the collection with the given items. 205 | * 206 | * @param array $items 207 | * 208 | * @throws MappingException 209 | * 210 | * @return self 211 | */ 212 | public function merge($items) 213 | { 214 | $dictionary = $this->getDictionary(); 215 | 216 | foreach ($items as $item) { 217 | $dictionary[$this->getEntityKey($item)] = $item; 218 | } 219 | 220 | return new static(array_values($dictionary)); 221 | } 222 | 223 | /** 224 | * Diff the collection with the given items. 225 | * 226 | * @param \ArrayAccess|array $items 227 | * 228 | * @return self 229 | */ 230 | public function diff($items) 231 | { 232 | $diff = new static(); 233 | 234 | $dictionary = $this->getDictionary($items); 235 | 236 | foreach ($this->items as $item) { 237 | if (!isset($dictionary[$this->getEntityKey($item)])) { 238 | $diff->add($item); 239 | } 240 | } 241 | 242 | return $diff; 243 | } 244 | 245 | /** 246 | * Intersect the collection with the given items. 247 | * 248 | * @param \ArrayAccess|array $items 249 | * 250 | * @throws MappingException 251 | * 252 | * @return self 253 | */ 254 | public function intersect($items) 255 | { 256 | $intersect = new static(); 257 | 258 | $dictionary = $this->getDictionary($items); 259 | 260 | foreach ($this->items as $item) { 261 | if (isset($dictionary[$this->getEntityKey($item)])) { 262 | $intersect->add($item); 263 | } 264 | } 265 | 266 | return $intersect; 267 | } 268 | 269 | /** 270 | * Returns only the models from the collection with the specified keys. 271 | * 272 | * @param mixed $keys 273 | * 274 | * @return self 275 | */ 276 | public function only($keys) 277 | { 278 | $dictionary = array_only($this->getDictionary(), $keys); 279 | 280 | return new static(array_values($dictionary)); 281 | } 282 | 283 | /** 284 | * Returns all models in the collection except the models with specified keys. 285 | * 286 | * @param mixed $keys 287 | * 288 | * @return self 289 | */ 290 | public function except($keys) 291 | { 292 | $dictionary = array_except($this->getDictionary(), $keys); 293 | 294 | return new static(array_values($dictionary)); 295 | } 296 | 297 | /** 298 | * Get a dictionary keyed by primary keys. 299 | * 300 | * @param \ArrayAccess|array $items 301 | * 302 | * @throws MappingException 303 | * 304 | * @return array 305 | */ 306 | public function getDictionary($items = null) 307 | { 308 | $items = is_null($items) ? $this->items : $items; 309 | 310 | $dictionary = []; 311 | 312 | foreach ($items as $value) { 313 | $dictionary[$this->getEntityKey($value)] = $value; 314 | } 315 | 316 | return $dictionary; 317 | } 318 | 319 | /** 320 | * @throws MappingException 321 | * 322 | * @return array 323 | */ 324 | public function getEntityKeys() 325 | { 326 | return array_keys($this->getDictionary()); 327 | } 328 | 329 | /** 330 | * @param $entity 331 | * 332 | * @throws MappingException 333 | * 334 | * @return mixed 335 | */ 336 | protected function getEntityKey($entity) 337 | { 338 | $keyName = Manager::getMapper($entity)->getEntityMap()->getKeyName(); 339 | 340 | $wrapper = $this->factory->make($entity); 341 | 342 | return $wrapper->getEntityAttribute($keyName); 343 | } 344 | 345 | /** 346 | * Get the max value of a given key. 347 | * 348 | * @param string|null $key 349 | * 350 | * @throws MappingException 351 | * 352 | * @return mixed 353 | */ 354 | public function max($key = null) 355 | { 356 | return $this->reduce(function ($result, $item) use ($key) { 357 | $wrapper = $this->factory->make($item); 358 | 359 | return (is_null($result) || $wrapper->getEntityAttribute($key) > $result) ? 360 | $wrapper->getEntityAttribute($key) : $result; 361 | }); 362 | } 363 | 364 | /** 365 | * Get the min value of a given key. 366 | * 367 | * @param string|null $key 368 | * 369 | * @throws MappingException 370 | * 371 | * @return mixed 372 | */ 373 | public function min($key = null) 374 | { 375 | return $this->reduce(function ($result, $item) use ($key) { 376 | $wrapper = $this->factory->make($item); 377 | 378 | return (is_null($result) || $wrapper->getEntityAttribute($key) < $result) 379 | ? $wrapper->getEntityAttribute($key) : $result; 380 | }); 381 | } 382 | 383 | /** 384 | * Get an array with the values of a given key. 385 | * 386 | * @param string $value 387 | * @param string|null $key 388 | * 389 | * @return \Illuminate\Support\Collection 390 | */ 391 | public function pluck($value, $key = null) 392 | { 393 | return new Collection(Arr::pluck($this->items, $value, $key)); 394 | } 395 | 396 | /** 397 | * Alias for the "pluck" method. 398 | * 399 | * @param string $value 400 | * @param string|null $key 401 | * 402 | * @return \Illuminate\Support\Collection 403 | */ 404 | public function lists($value, $key = null) 405 | { 406 | return $this->pluck($value, $key); 407 | } 408 | 409 | /** 410 | * Unset the item at a given offset. 411 | * 412 | * @param string $key 413 | * 414 | * @return void 415 | */ 416 | public function offsetUnset($key) 417 | { 418 | $this->items = array_filter($this->items, function ($item) use ($key) { 419 | $primaryKey = $this->getEntityKey($item); 420 | 421 | return $primaryKey !== $key; 422 | }); 423 | } 424 | 425 | /** 426 | * Get a base Support collection instance from this collection. 427 | * 428 | * @return \Illuminate\Support\Collection 429 | */ 430 | public function toBase() 431 | { 432 | return new Collection($this->items); 433 | } 434 | 435 | public function toArray() 436 | { 437 | return array_values(parent::toArray()); 438 | } 439 | 440 | /** 441 | * Get the collection of items as JSON. 442 | * 443 | * @param int $options 444 | * 445 | * @return string 446 | */ 447 | public function toJson($options = 0) 448 | { 449 | $collection = new Collection(array_values($this->items)); 450 | 451 | return $collection->toJson($options); 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /src/Relationships/Relationship.php: -------------------------------------------------------------------------------- 1 | relatedMapper = $mapper; 103 | 104 | $this->query = $mapper->getQuery(); 105 | 106 | $this->factory = new Factory(); 107 | 108 | $this->parent = $this->factory->make($parent); 109 | 110 | $this->parentMapper = $mapper->getManager()->getMapper($parent); 111 | 112 | $this->parentMap = $this->parentMapper->getEntityMap(); 113 | 114 | $this->related = $mapper->newInstance(); 115 | 116 | $this->relatedMap = $mapper->getEntityMap(); 117 | 118 | $this->addConstraints(); 119 | } 120 | 121 | /** 122 | * Indicate if the relationship uses a pivot table. 123 | * 124 | * @return bool 125 | */ 126 | public function hasPivot() 127 | { 128 | return static::$hasPivot; 129 | } 130 | 131 | /** 132 | * Set the base constraints on the relation query. 133 | * 134 | * @return void 135 | */ 136 | abstract public function addConstraints(); 137 | 138 | /** 139 | * Set the constraints for an eager load of the relation. 140 | * 141 | * @param array $results 142 | * 143 | * @return void 144 | */ 145 | abstract public function addEagerConstraints(array $results); 146 | 147 | /** 148 | * Match the eagerly loaded results to their parents, then return 149 | * updated results. 150 | * 151 | * @param array $results 152 | * @param string $relation 153 | * 154 | * @return array 155 | */ 156 | abstract public function match(array $results, $relation); 157 | 158 | /** 159 | * Get the results of the relationship. 160 | * 161 | * @param string $relation relation name in parent's entity map 162 | * 163 | * @return mixed 164 | */ 165 | abstract public function getResults($relation); 166 | 167 | /** 168 | * Get the relationship for eager loading. 169 | * 170 | * @return \Illuminate\Support\Collection 171 | */ 172 | public function getEager() 173 | { 174 | return $this->get(); 175 | } 176 | 177 | /** 178 | * Add the constraints for a relationship count query. 179 | * 180 | * @param Query $query 181 | * @param Query $parent 182 | * 183 | * @return Query 184 | */ 185 | public function getRelationCountQuery(Query $query, Query $parent) 186 | { 187 | $query->select(new Expression('count(*)')); 188 | 189 | $key = $this->wrap($this->getQualifiedParentKeyName()); 190 | 191 | return $query->where($this->getHasCompareKey(), '=', new Expression($key)); 192 | } 193 | 194 | /** 195 | * Run a callback with constraints disabled on the relation. 196 | * 197 | * @param Closure $callback 198 | * 199 | * @return mixed 200 | */ 201 | public static function noConstraints(Closure $callback) 202 | { 203 | static::$constraints = false; 204 | 205 | // When resetting the relation where clause, we want to shift the first element 206 | // off of the bindings, leaving only the constraints that the developers put 207 | // as "extra" on the relationships, and not original relation constraints. 208 | $results = call_user_func($callback); 209 | 210 | static::$constraints = true; 211 | 212 | return $results; 213 | } 214 | 215 | /** 216 | * Get all of the primary keys for an array of entities. 217 | * 218 | * @param array $entities 219 | * @param string $key 220 | * 221 | * @return array 222 | */ 223 | protected function getKeys(array $entities, $key = null) 224 | { 225 | if (is_null($key)) { 226 | $key = $this->relatedMap->getKeyName(); 227 | } 228 | 229 | $host = $this; 230 | 231 | return array_unique(array_values(array_map(function ($value) use ($key, $host) { 232 | if (!$value instanceof InternallyMappable) { 233 | $value = $host->factory->make($value); 234 | } 235 | 236 | return $value->getEntityAttribute($key); 237 | }, $entities))); 238 | } 239 | 240 | /** 241 | * Get all the keys from a result set. 242 | * 243 | * @param array $results 244 | * @param string $key 245 | * 246 | * @return array 247 | */ 248 | protected function getKeysFromResults(array $results, $key = null) 249 | { 250 | if (is_null($key)) { 251 | $key = $this->parentMap->getKeyName(); 252 | } 253 | 254 | return array_unique(array_values(array_map(function ($value) use ($key) { 255 | return $value[$key]; 256 | }, $results))); 257 | } 258 | 259 | /** 260 | * Get the underlying query for the relation. 261 | * 262 | * @return Query 263 | */ 264 | public function getQuery() 265 | { 266 | return $this->query; 267 | } 268 | 269 | /** 270 | * Get the base query builder. 271 | * 272 | * @return \Illuminate\Database\Query\Builder 273 | */ 274 | public function getBaseQuery() 275 | { 276 | return $this->query->getQuery(); 277 | } 278 | 279 | /** 280 | * Get the parent model of the relation. 281 | * 282 | * @return InternallyMappable 283 | */ 284 | public function getParent() 285 | { 286 | return $this->parent; 287 | } 288 | 289 | /** 290 | * Set the parent model of the relation. 291 | * 292 | * @param InternallyMappable $parent 293 | * 294 | * @return void 295 | */ 296 | public function setParent(InternallyMappable $parent) 297 | { 298 | $this->parent = $parent; 299 | } 300 | 301 | /** 302 | * Get the fully qualified parent key name. 303 | * 304 | * @return string 305 | */ 306 | protected function getQualifiedParentKeyName() 307 | { 308 | return $this->parent->getQualifiedKeyName(); 309 | } 310 | 311 | /** 312 | * Get the related entity of the relation. 313 | * 314 | * @return \Analogue\ORM\Entity 315 | */ 316 | public function getRelated() 317 | { 318 | return $this->related; 319 | } 320 | 321 | /** 322 | * Get the related mapper for the relation. 323 | * 324 | * @return Mapper 325 | */ 326 | public function getRelatedMapper() 327 | { 328 | return $this->relatedMapper; 329 | } 330 | 331 | /** 332 | * Get the name of the "created at" column. 333 | * 334 | * @return string 335 | */ 336 | public function createdAt() 337 | { 338 | return $this->parentMap->getCreatedAtColumn(); 339 | } 340 | 341 | /** 342 | * Get the name of the "updated at" column. 343 | * 344 | * @return string 345 | */ 346 | public function updatedAt() 347 | { 348 | return $this->parentMap->getUpdatedAtColumn(); 349 | } 350 | 351 | /** 352 | * Get the name of the related model's "updated at" column. 353 | * 354 | * @return string 355 | */ 356 | public function relatedUpdatedAt() 357 | { 358 | return $this->related->getUpdatedAtColumn(); 359 | } 360 | 361 | /** 362 | * Wrap the given value with the parent query's grammar. 363 | * 364 | * @param string $value 365 | * 366 | * @return string 367 | */ 368 | public function wrap($value) 369 | { 370 | return $this->parentMapper->getQuery()->getQuery()->getGrammar()->wrap($value); 371 | } 372 | 373 | /** 374 | * Get a fresh timestamp. 375 | * 376 | * @return Carbon 377 | */ 378 | protected function freshTimestamp() 379 | { 380 | return new Carbon(); 381 | } 382 | 383 | /** 384 | * Cache the link between parent and related 385 | * into the mapper's Entity Cache. 386 | * 387 | * @param EntityCollection|Mappable $results result of the relation query 388 | * @param string $relation name of the relation method on the parent entity 389 | * 390 | * @return void 391 | */ 392 | protected function cacheRelation($results, $relation) 393 | { 394 | $cache = $this->parentMapper->getEntityCache(); 395 | 396 | $cache->cacheLoadedRelationResult($this->parent->getEntityKeyName(), $relation, $results, $this); 397 | } 398 | 399 | /** 400 | * Return Pivot attributes when available on a relationship. 401 | * 402 | * @return array 403 | */ 404 | public function getPivotAttributes() 405 | { 406 | return []; 407 | } 408 | 409 | /** 410 | * Get a combo type.primaryKey. 411 | * 412 | * @param Mappable $entity 413 | * 414 | * @return string 415 | */ 416 | protected function getEntityHash(Mappable $entity) 417 | { 418 | $class = get_class($entity); 419 | 420 | $keyName = Mapper::getMapper($class)->getEntityMap()->getKeyName(); 421 | 422 | return $class.'.'.$entity->getEntityAttribute($keyName); 423 | } 424 | 425 | /** 426 | * Run synchronization content if needed by the 427 | * relation type. 428 | * 429 | * @param array $actualContent 430 | * 431 | * @return void 432 | */ 433 | abstract public function sync(array $actualContent); 434 | 435 | /** 436 | * Handle dynamic method calls to the relationship. 437 | * 438 | * @param string $method 439 | * @param array $parameters 440 | * 441 | * @return mixed 442 | */ 443 | public function __call($method, $parameters) 444 | { 445 | $result = call_user_func_array([$this->query, $method], $parameters); 446 | 447 | if ($result === $this->query) { 448 | return $this; 449 | } 450 | 451 | return $result; 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /src/System/Cache/AttributeCache.php: -------------------------------------------------------------------------------- 1 | Entity instance correspondancy. 29 | * 30 | * @var array 31 | */ 32 | protected $instances = []; 33 | 34 | /** 35 | * Entity Map for the current Entity Type. 36 | * 37 | * @var \Analogue\ORM\EntityMap 38 | */ 39 | protected $entityMap; 40 | 41 | /** 42 | * Wrapper factory. 43 | * 44 | * @var \Analogue\ORM\System\Wrappers\Factory 45 | */ 46 | protected $factory; 47 | 48 | /** 49 | * Associative array containing list of pivot attributes per relationship 50 | * so we don't have to call relationship method on refresh. 51 | * 52 | * @var array 53 | */ 54 | protected $pivotAttributes = []; 55 | 56 | /** 57 | * EntityCache constructor. 58 | * 59 | * @param EntityMap $entityMap 60 | */ 61 | public function __construct(EntityMap $entityMap) 62 | { 63 | $this->entityMap = $entityMap; 64 | 65 | $this->factory = new Factory(); 66 | } 67 | 68 | /** 69 | * Add an array of key=>attributes representing 70 | * the initial state of loaded entities. 71 | * 72 | * @param array $results 73 | */ 74 | public function add(array $results) 75 | { 76 | $cachedResults = []; 77 | 78 | $keyColumn = $this->entityMap->getKeyName(); 79 | 80 | foreach ($results as $result) { 81 | $id = $result[$keyColumn]; 82 | 83 | // Forget the ID field from the cache attributes 84 | // to prevent any side effect. 85 | // TODO : remove primary key check from dirty attributes parsing 86 | //unset($result[$keyColumn]); 87 | $cachedResults[$id] = $this->rawResult($result); 88 | } 89 | 90 | if (empty($this->cache)) { 91 | $this->cache = $cachedResults; 92 | } else { 93 | $this->mergeCacheResults($cachedResults); 94 | } 95 | } 96 | 97 | /** 98 | * Return result without any collection or object. 99 | * 100 | * @param array $result 101 | * 102 | * @return array 103 | */ 104 | protected function rawResult(array $result): array 105 | { 106 | return $result; 107 | 108 | return array_filter($result, function ($attribute) { 109 | return !is_object($attribute); 110 | }); 111 | } 112 | 113 | /** 114 | * Retrieve initial attributes for a single entity. 115 | * 116 | * @param string $id 117 | * 118 | * @return array 119 | */ 120 | public function get(string $id = null): array 121 | { 122 | if ($this->has($id)) { 123 | return $this->cache[$id]; 124 | } 125 | 126 | return []; 127 | } 128 | 129 | /** 130 | * Check if a record for this id exists. 131 | * 132 | * @param string $id 133 | * 134 | * @return bool 135 | */ 136 | public function has(string $id = null): bool 137 | { 138 | return array_key_exists($id, $this->cache); 139 | } 140 | 141 | /** 142 | * Combine new result set with existing attributes in 143 | * cache. 144 | * 145 | * @param array $results 146 | * 147 | * @return void 148 | */ 149 | protected function mergeCacheResults(array $results) 150 | { 151 | foreach ($results as $key => $entity) { 152 | $this->cache[$key] = $entity; 153 | } 154 | } 155 | 156 | /** 157 | * Cache Relation's query result for an entity. 158 | * 159 | * @param string $key primary key of the cached entity 160 | * @param string $relation name of the relation 161 | * @param mixed $results results of the relationship's query 162 | * @param Relationship $relationship 163 | * 164 | * @throws MappingException 165 | * 166 | * @return void 167 | */ 168 | public function cacheLoadedRelationResult(string $key, string $relation, $results, Relationship $relationship) 169 | { 170 | if ($results instanceof EntityCollection) { 171 | $this->cacheManyRelationResults($key, $relation, $results, $relationship); 172 | } 173 | 174 | // TODO : As we support popo Entities, Maybe this check isn't needed anymore, 175 | // or we have to check that $result is an object instead 176 | if ($results instanceof Mappable) { 177 | $this->cacheSingleRelationResult($key, $relation, $results, $relationship); 178 | } 179 | } 180 | 181 | /** 182 | * Create a cachedRelationship instance which will hold related entity's hash and pivot attributes, if any. 183 | * 184 | * @param string $relation 185 | * @param array $result 186 | * @param Relationship $relationship 187 | * 188 | * @throws MappingException 189 | * 190 | * @return CachedRelationship 191 | */ 192 | protected function getCachedRelationship(string $relation, $result, Relationship $relationship) 193 | { 194 | $pivotColumns = $relationship->getPivotAttributes(); 195 | 196 | if (!array_key_exists($relation, $this->pivotAttributes)) { 197 | $this->pivotAttributes[$relation] = $pivotColumns; 198 | } 199 | 200 | $wrapper = $this->factory->make($result); 201 | 202 | $hash = $wrapper->getEntityHash(); 203 | 204 | if (count($pivotColumns) > 0) { 205 | $pivotAttributes = []; 206 | foreach ($pivotColumns as $column) { 207 | $pivot = $wrapper->getEntityAttribute('pivot'); 208 | 209 | $pivotWrapper = $this->factory->make($pivot); 210 | 211 | $pivotAttributes[$column] = $pivotWrapper->getEntityAttribute($column); 212 | } 213 | 214 | $cachedRelationship = new CachedRelationship($hash, $pivotAttributes); 215 | } else { 216 | $cachedRelationship = new CachedRelationship($hash); 217 | } 218 | 219 | return $cachedRelationship; 220 | } 221 | 222 | /** 223 | * Cache a many relationship. 224 | * 225 | * @param string $parentKey 226 | * @param string $relation 227 | * @param EntityCollection $results 228 | * @param Relationship $relationship 229 | * 230 | * @throws MappingException 231 | */ 232 | protected function cacheManyRelationResults(string $parentKey, string $relation, $results, Relationship $relationship) 233 | { 234 | $this->cache[$parentKey][$relation] = []; 235 | 236 | foreach ($results as $result) { 237 | $cachedRelationship = $this->getCachedRelationship($relation, $result, $relationship); 238 | 239 | $relatedHash = $cachedRelationship->getHash(); 240 | 241 | $this->cache[$parentKey][$relation][$relatedHash] = $cachedRelationship; 242 | } 243 | } 244 | 245 | /** 246 | * Cache a single relationship. 247 | * 248 | * @param string $parentKey 249 | * @param string $relation 250 | * @param Mappable $result 251 | * @param Relationship $relationship 252 | * 253 | * @throws MappingException 254 | */ 255 | protected function cacheSingleRelationResult(string $parentKey, string $relation, $result, Relationship $relationship) 256 | { 257 | $this->cache[$parentKey][$relation] = $this->getCachedRelationship($relation, $result, $relationship); 258 | } 259 | 260 | /** 261 | * Get Entity's Hash. 262 | * 263 | * @param $entity 264 | * 265 | * @throws MappingException 266 | * 267 | * @return string 268 | */ 269 | protected function getEntityHash(InternallyMappable $entity): string 270 | { 271 | $class = $entity->getEntityClass(); 272 | 273 | $mapper = Manager::getMapper($class); 274 | 275 | $keyName = $mapper->getEntityMap()->getKeyName(); 276 | 277 | return $class.'.'.$entity->getEntityAttribute($keyName); 278 | } 279 | 280 | /** 281 | * Refresh the cache record for an aggregated entity after a write operation. 282 | * 283 | * @param Aggregate $entity 284 | */ 285 | public function refresh(Aggregate $entity) 286 | { 287 | $this->cache[$entity->getEntityKeyValue()] = $this->transform($entity); 288 | } 289 | 290 | /** 291 | * Transform an Aggregated Entity into a cache record. 292 | * 293 | * @param Aggregate $aggregatedEntity 294 | * 295 | * @throws MappingException 296 | * 297 | * @return array 298 | */ 299 | protected function transform(Aggregate $aggregatedEntity): array 300 | { 301 | $baseAttributes = $aggregatedEntity->getRawAttributes(); 302 | 303 | $relationAttributes = []; 304 | 305 | // First we'll handle each relationships that are a one to one 306 | // relation, and which will be saved as a CachedRelationship 307 | // object inside the cache. 308 | 309 | // NOTE : storing localRelationships maybe useless has we store 310 | // the foreign key in the attributes already. 311 | 312 | foreach ($this->entityMap->getSingleRelationships() as $relation) { 313 | $aggregates = $aggregatedEntity->getRelationship($relation); 314 | 315 | if (count($aggregates) == 1) { 316 | $related = $aggregates[0]; 317 | $relationAttributes[$relation] = new CachedRelationship($related->getEntityHash()); 318 | } 319 | if (count($aggregates) > 1) { 320 | throw new MappingException("Single Relationship '$relation' contains several related entities"); 321 | } 322 | } 323 | 324 | // Then we'll handle the 'many' relationships and store them as 325 | // an array of CachedRelationship objects. 326 | 327 | foreach ($this->entityMap->getManyRelationships() as $relation) { 328 | $aggregates = $aggregatedEntity->getRelationship($relation); 329 | 330 | $relationAttributes[$relation] = []; 331 | 332 | foreach ($aggregates as $aggregate) { 333 | $relationAttributes[$relation][] = new CachedRelationship( 334 | $aggregate->getEntityHash(), 335 | $aggregate->getPivotAttributes() 336 | ); 337 | } 338 | } 339 | 340 | return $baseAttributes + $relationAttributes; 341 | } 342 | 343 | /** 344 | * Get pivot attributes for a relation. 345 | * 346 | * @param string $relation 347 | * @param InternallyMappable $entity 348 | * 349 | * @return array 350 | */ 351 | protected function getPivotValues(string $relation, InternallyMappable $entity): array 352 | { 353 | $values = []; 354 | 355 | $entityAttributes = $entity->getEntityAttributes(); 356 | 357 | if (array_key_exists($relation, $this->pivotAttributes)) { 358 | foreach ($this->pivotAttributes[$relation] as $attribute) { 359 | if (array_key_exists($attribute, $entityAttributes)) { 360 | $values[$attribute] = $entity->getEntityAttribute('pivot')->$attribute; 361 | } 362 | } 363 | } 364 | 365 | return $values; 366 | } 367 | 368 | /** 369 | * Clear the entity Cache. Use with caution as it could result 370 | * in unpredictable behaviour if the cached entities are stored 371 | * after the cache clear operation. 372 | * 373 | * @return void 374 | */ 375 | public function clear() 376 | { 377 | $this->cache = []; 378 | $this->pivotAttributes = []; 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/System/Builders/ResultBuilder.php: -------------------------------------------------------------------------------- 1 | mapper = $mapper; 56 | $this->entityMap = $mapper->getEntityMap(); 57 | $this->useCache = $useCache; 58 | } 59 | 60 | /** 61 | * Convert a result set into an array of entities. 62 | * 63 | * @param array $results The results to convert into entities. 64 | * @param array $eagerLoads name of the relation(s) to be eager loaded on the Entities 65 | * 66 | * @return array 67 | */ 68 | public function build(array $results, array $eagerLoads) 69 | { 70 | // First, we'll cache the raw result set 71 | $this->cacheResults($results); 72 | 73 | // Parse embedded relations and build corresponding entities using the default 74 | // mapper. 75 | $results = $this->buildEmbeddedRelationships($results); 76 | 77 | // Launch the queries related to eager loads, and match the 78 | // current result set to these loaded relationships. 79 | $results = $this->queryEagerLoadedRelationships($results, $eagerLoads); 80 | 81 | return $this->buildResultSet($results); 82 | } 83 | 84 | /** 85 | * Cache result set. 86 | * 87 | * @param array $results 88 | * 89 | * @return void 90 | */ 91 | protected function cacheResults(array $results) 92 | { 93 | $mapper = $this->mapper; 94 | 95 | // When hydrating EmbeddedValue object, they'll likely won't 96 | // have a primary key set. 97 | if ($mapper->getEntityMap()->getKeyName() !== null) { 98 | $mapper->getEntityCache()->add($results); 99 | } 100 | } 101 | 102 | /** 103 | * Build embedded objects and match them to the result set. 104 | * 105 | * @param array $results 106 | * 107 | * @return array 108 | */ 109 | protected function buildEmbeddedRelationships(array $results): array 110 | { 111 | $entityMap = $this->entityMap; 112 | $instance = $this->mapper->newInstance(); 113 | $embeddeds = $entityMap->getEmbeddedRelationships(); 114 | 115 | foreach ($embeddeds as $embedded) { 116 | $results = $entityMap->$embedded($instance)->match($results, $embedded); 117 | } 118 | 119 | return $results; 120 | } 121 | 122 | /** 123 | * Launch queries on eager loaded relationships. 124 | * 125 | * @param array $results 126 | * @param array $eagerLoads 127 | * 128 | * @return array 129 | */ 130 | protected function queryEagerLoadedRelationships(array $results, array $eagerLoads): array 131 | { 132 | $this->eagerLoads = $this->parseRelations($eagerLoads); 133 | 134 | return $this->eagerLoadRelations($results); 135 | } 136 | 137 | /** 138 | * Parse a list of relations into individuals. 139 | * 140 | * @param array $relations 141 | * 142 | * @return array 143 | */ 144 | protected function parseRelations(array $relations): array 145 | { 146 | $results = []; 147 | 148 | foreach ($relations as $name => $constraints) { 149 | // If the "relation" value is actually a numeric key, we can assume that no 150 | // constraints have been specified for the eager load and we'll just put 151 | // an empty Closure with the loader so that we can treat all the same. 152 | if (is_numeric($name)) { 153 | $f = function () { 154 | }; 155 | 156 | list($name, $constraints) = [$constraints, $f]; 157 | } 158 | 159 | // We need to separate out any nested includes. Which allows the developers 160 | // to load deep relationships using "dots" without stating each level of 161 | // the relationship with its own key in the array of eager load names. 162 | $results = $this->parseNested($name, $results); 163 | 164 | $results[$name] = $constraints; 165 | } 166 | 167 | return $results; 168 | } 169 | 170 | /** 171 | * Parse the nested relationships in a relation. 172 | * 173 | * @param string $name 174 | * @param array $results 175 | * 176 | * @return array 177 | */ 178 | protected function parseNested(string $name, array $results): array 179 | { 180 | $progress = []; 181 | 182 | // If the relation has already been set on the result array, we will not set it 183 | // again, since that would override any constraints that were already placed 184 | // on the relationships. We will only set the ones that are not specified. 185 | foreach (explode('.', $name) as $segment) { 186 | $progress[] = $segment; 187 | 188 | if (!isset($results[$last = implode('.', $progress)])) { 189 | $results[$last] = function () { 190 | }; 191 | } 192 | } 193 | 194 | return $results; 195 | } 196 | 197 | /** 198 | * Eager load the relationships on a result set. 199 | * 200 | * @param array $results 201 | * 202 | * @return array 203 | */ 204 | public function eagerLoadRelations(array $results): array 205 | { 206 | foreach ($this->eagerLoads as $name => $constraints) { 207 | // First, we'll check if the entity map has a relation and just pass if it 208 | // is not the case 209 | 210 | if (!in_array($name, $this->entityMap->getRelationships())) { 211 | continue; 212 | } 213 | 214 | // For nested eager loads we'll skip loading them here and they will be set as an 215 | // eager load on the query to retrieve the relation so that they will be eager 216 | // loaded on that query, because that is where they get hydrated as models. 217 | if (strpos($name, '.') === false) { 218 | $results = $this->loadRelation($results, $name, $constraints); 219 | } 220 | } 221 | 222 | return $results; 223 | } 224 | 225 | /** 226 | * Eagerly load the relationship on a set of entities. 227 | * 228 | * @param array $results 229 | * @param string $name 230 | * @param \Closure $constraints 231 | * 232 | * @return array 233 | */ 234 | protected function loadRelation(array $results, string $name, Closure $constraints): array 235 | { 236 | // First we will "back up" the existing where conditions on the query so we can 237 | // add our eager constraints. Then we will merge the wheres that were on the 238 | // query back to it in order that any where conditions might be specified. 239 | $relation = $this->getRelation($name); 240 | 241 | $relation->addEagerConstraints($results); 242 | 243 | call_user_func($constraints, $relation); 244 | 245 | // Once we have the results, we just match those back up to their parent models 246 | // using the relationship instance. Then we just return the finished arrays 247 | // of models which have been eagerly hydrated and are readied for return. 248 | return $relation->match($results, $name); 249 | } 250 | 251 | /** 252 | * Get the relation instance for the given relation name. 253 | * 254 | * @param string $relation 255 | * 256 | * @return \Analogue\ORM\Relationships\Relationship 257 | */ 258 | public function getRelation(string $relation): Relationship 259 | { 260 | // We want to run a relationship query without any constrains so that we will 261 | // not have to remove these where clauses manually which gets really hacky 262 | // and is error prone while we remove the developer's own where clauses. 263 | $query = Relationship::noConstraints(function () use ($relation) { 264 | return $this->entityMap->$relation($this->mapper->newInstance()); 265 | }); 266 | 267 | $nested = $this->nestedRelations($relation); 268 | 269 | // If there are nested relationships set on the query, we will put those onto 270 | // the query instances so that they can be handled after this relationship 271 | // is loaded. In this way they will all trickle down as they are loaded. 272 | if (count($nested) > 0) { 273 | $query->getQuery()->with($nested); 274 | } 275 | 276 | return $query; 277 | } 278 | 279 | /** 280 | * Get the deeply nested relations for a given top-level relation. 281 | * 282 | * @param string $relation 283 | * 284 | * @return array 285 | */ 286 | protected function nestedRelations(string $relation): array 287 | { 288 | $nested = []; 289 | 290 | // We are basically looking for any relationships that are nested deeper than 291 | // the given top-level relationship. We will just check for any relations 292 | // that start with the given top relations and adds them to our arrays. 293 | foreach ($this->eagerLoads as $name => $constraints) { 294 | if ($this->isNested($name, $relation)) { 295 | $nested[substr($name, strlen($relation.'.'))] = $constraints; 296 | } 297 | } 298 | 299 | return $nested; 300 | } 301 | 302 | /** 303 | * Determine if the relationship is nested. 304 | * 305 | * @param string $name 306 | * @param string $relation 307 | * 308 | * @return bool 309 | */ 310 | protected function isNested(string $name, string $relation): bool 311 | { 312 | $dots = str_contains($name, '.'); 313 | 314 | return $dots && starts_with($name, $relation.'.'); 315 | } 316 | 317 | /** 318 | * Build an entity from results. 319 | * 320 | * @param array $results 321 | * 322 | * @return array 323 | */ 324 | protected function buildResultSet(array $results): array 325 | { 326 | return $this->buildUnkeyedResultSet($results); 327 | } 328 | 329 | /** 330 | * Build a result set. 331 | * 332 | * @param array $results 333 | * 334 | * @return array 335 | */ 336 | protected function buildUnkeyedResultSet(array $results): array 337 | { 338 | $builder = new EntityBuilder($this->mapper, array_keys($this->eagerLoads), $this->useCache); 339 | 340 | return array_map(function ($item) use ($builder) { 341 | return $builder->build($item); 342 | }, $results); 343 | } 344 | 345 | /** 346 | * Build a result set keyed by PK. 347 | * 348 | * @param array $results 349 | * @param string. $primaryKey 350 | * 351 | * @return array 352 | */ 353 | protected function buildKeyedResultSet(array $results, string $primaryKey): array 354 | { 355 | $builder = new EntityBuilder($this->mapper, array_keys($this->eagerLoads), $this->useCache); 356 | 357 | $keys = array_map(function ($item) use ($primaryKey) { 358 | return $item[$primaryKey]; 359 | }, $results); 360 | 361 | return array_combine($keys, array_map(function ($item) use ($builder) { 362 | return $builder->build($item); 363 | }, $results)); 364 | } 365 | } 366 | --------------------------------------------------------------------------------