$relations Relations to normalize. * @return array Relations definition. */ public static function normalizeRelationsDefinition(array $relations): array { // Normalize relations array. foreach ($relations as $key => $relation) { // Add each relation to eager load to the with array. if (!is_string($key)) { // If key is an integer, then $relation is a string key. $relations[$relation] = true; unset($relations[$key]); } } // Return normalized relations array. return $relations; } /** * Relations that should be eager loaded. * @var array */ protected array $with = []; /** * @inheritDoc * @param Entity $entity Reference entity. */ public function __construct(Database $database, string $table, protected Entity $entity) { parent::__construct($database, $table); } /** * Create a new entity instance. * @return Entity Entity instance. */ protected function newEntity(): Entity { // Create a new entity instance. return $this->entity->new(); } /** * Get queried entity primary key name. * @return string Primary key name. */ public function getPrimaryKeyName(): string { return array_first_or_val($this->entity->getPrimaryFields()); } /** * Add a where condition for the primary key of the entity. * @param mixed|array $keyValue Entity primary key value(s) to match. * @return $this */ public function whereKey(mixed $keyValue): static { return $this->whereIn($this->getPrimaryKeyName(), is_array($keyValue) ? $keyValue : [$keyValue]); } /** * Make a relation prefix for the given relation. * @param string $relationKey Relation key. * @return string Relation prefix. */ private function getRelationPrefix(string $relationKey): string { return "{$this->entity->getTableName()}_{$relationKey}_"; } /** * Go through all first-level relations to eager load and add joins and selected columns for them. * @param bool $generateSelect Set if SELECTed columns must be generated or not. * @return string[] Loaded inline relations. * @throws UndefinedRelationException */ private function addInlineRelations(bool $generateSelect): array { if (empty($this->with)) return []; // Initialize list of inline loaded relations. $inlineRelations = []; foreach ($this->with as $relationKey => $relationCallable) { // For each relation, if it's inline, append a JOIN and SELECT for it. // Get the required relation. $relation = $this->entity->getRelation($relationKey); if ($relation->canInlineLoad()) { // If the relation is not multiple, it can be retrieved inline. $relation->setupInlineLoading($this); if ($generateSelect) // Generate SELECT for the relation. $this->select(...$relation->newEntity()->sqlSelectFields($this->getRelationPrefix($relationKey))); if (is_callable($relationCallable)) // Call the callable with the alterable query. $relationCallable($this); // Add to inline loaded relations. $inlineRelations[] = $relationKey; } } return $inlineRelations; } /** * Get queried entities. * @param string|null $indexProperty A property to use as entities array index. * @return Entity[]|array Retrieved entities. * @throws MissingConditionValueException * @throws IncompatibleTypeException * @throws InvalidTypeException * @throws UndefinedRelationException * @throws UnknownDatabaseException * @throws UnhandledPropertyTypeException */ public function get(?string $indexProperty = null): array { // If there's no custom SELECT, generate it. $generateSelect = empty($this->selected); if ($generateSelect) // Generate SELECT for the main entity. $this->select(...$this->entity->sqlSelectFields()); // Add inline relations retrieval to the query. $inlineRelations = $this->addInlineRelations($generateSelect); // Map entities with their inline relations, if there are some. $entities = $this->mapEntities($this->execute(), $inlineRelations, $indexProperty); // Load all remaining relations (remove already loaded inline relations). $relationsToLoad = $this->with ?? []; foreach ($inlineRelations as $inlineRelation) unset($relationsToLoad[$inlineRelation]); $this->load($entities, $relationsToLoad); return $entities; } /** * Retrieve the entities with the given keys. * @param mixed[] $keysValues Entity primary keys values to match. * @return Entity[] Retrieved entities. * @throws IncompatibleTypeException * @throws InvalidTypeException * @throws MissingConditionValueException * @throws UndefinedRelationException * @throws UnhandledPropertyTypeException * @throws UnknownDatabaseException */ public function findMany(array $keysValues): array { return $this->whereKey($keysValues)->get(); } /** * Retrieve the first entity of the query. * @return Entity Retrieved entity. * @throws IncompatibleTypeException * @throws InvalidTypeException * @throws MissingConditionValueException * @throws UndefinedRelationException * @throws UnhandledPropertyTypeException * @throws UnknownDatabaseException */ public function first(): Entity { return array_first($this->limit(1)->get()); } /** * Retrieve the entity with the given key. * @param mixed $keyValue Primary key value of the entity. * @return Entity Retrieved entity. * @throws IncompatibleTypeException * @throws InvalidTypeException * @throws MissingConditionValueException * @throws UndefinedRelationException * @throws UnhandledPropertyTypeException * @throws UnknownDatabaseException */ public function find(mixed $keyValue): Entity { return $this->whereKey($keyValue)->first(); } /** * Map raw objects results to entities. * @param object[] $objects Raw objects results. * @param string[] $inlineRelations Inline relations to map, if there are some. * @return Entity[] The entities. * @throws IncompatibleTypeException * @throws InvalidTypeException * @throws UndefinedRelationException * @throws UnhandledPropertyTypeException */ protected function mapEntities(array $objects, array $inlineRelations = [], ?string $indexProperty = null): array { // Initialize entities array. $entities = []; foreach ($objects as $object) { // Map each object to an entity. $entity = $this->mapEntity($object); // Append to entities array, depending on index property name. if (empty($indexProperty)) $entities[] = $entity; else { if (empty($object->$indexProperty)) $object->$indexProperty = []; $entities[$object->$indexProperty][] = $entity; } // Map inline relations. foreach ($inlineRelations as $inlineRelation) { // For each inline relation, map its data. $relation = $this->entity->getRelation($inlineRelation); // Create a new entity as property value. $propertyValue = $relation->newEntity(); $propertyValue->fromSqlProperties($object, $this->getRelationPrefix($inlineRelation)); // Set the relation property. $entity->setOriginalProperty($inlineRelation, $propertyValue); } } // Return parsed entities. return $entities; } /** * Map raw object result to an entity. * @param object $object Raw object. * @return Entity The entity. * @throws IncompatibleTypeException * @throws InvalidTypeException */ protected function mapEntity(object $object): Entity { // Create a new entity. $entity = $this->newEntity(); // Read entity from SQL properties. $entity->fromSqlProperties($object); // Return parsed entity. return $entity; } /** * Eager load given relations. * @param Entity[] $entities Entities for which to load relations. * @param array $relationsToLoad Relations to load. * @return void * @throws IncompatibleTypeException * @throws InvalidTypeException * @throws MissingConditionValueException * @throws UndefinedRelationException * @throws UnknownDatabaseException * @throws UnhandledPropertyTypeException */ public function load(array $entities, array $relationsToLoad): void { foreach ($relationsToLoad as $relationKey => $relationCallable) { // For each relation to load... // Get relation instance. $relation = $this->entity->getRelation($relationKey); // Build base query to retrieve relations. $query = $relation->queryFor($entities); // Apply query modifier callable. if (is_callable($relationCallable)) $relationCallable($query); // Retrieve models and associate them with the right entities. $models = $query->get("__reference_key"); foreach ($entities as &$entity) { // Associate the retrieved model(s) to every entity. if (!empty($models[$entityId = $entity->{array_first_or_val($entity->getPrimaryFields())}])) $entity->setOriginalProperty($relationKey, $relation->isMultiple() ? $models[$entityId] : array_first($models[$entityId])); } } } /** * Add relations to eager load. * @param array $relations Relations that should be eager loaded. * @return $this */ public function with(array $relations): static { // Normalize relations array and add them in the with array. array_merge($this->with, EntityQuery::normalizeRelationsDefinition($relations)); return $this; } /** * Add relations to NOT eager load. * @param string[] $relations Relations that shouldn't be eager loaded. * @return $this */ public function without(array $relations): static { foreach ($relations as $relation) { // For each relation to NOT eager load, remove it from the with array. if (!empty($this->with[$relation])) unset($this->with[$relation]); } return $this; } }