entityManager = $entityManager; $this->entity = $entity; $this->hookMediator = $hookMediator; if (!$entity->getId()) { throw new RuntimeException("Can't use an entity w/o ID."); } if (!$entity->hasRelation($relationName)) { throw new RuntimeException("Entity does not have a relation '{$relationName}'."); } $this->relationName = $relationName; $this->relationType = $entity->getRelationType($relationName); $this->foreignEntityType = $entity->getRelationParam($relationName, 'entity'); $this->entityType = $entity->getEntityType(); if ($this->isBelongsToParentType()) { $this->noBuilder = true; } } protected function createBuilder(?Select $query = null): Builder { if ($this->noBuilder) { throw new RuntimeException("Can't use query builder for the '{$this->relationType}' relation type."); } return new Builder($this->entityManager, $this->entity, $this->relationName, $query); } /** * Clone a query. */ public function clone(Select $query): Builder { if ($this->noBuilder) { throw new RuntimeException("Can't use clone for the '{$this->relationType}' relation type."); } if ($query->getFrom() !== $this->foreignEntityType) { throw new RuntimeException("Passed query doesn't match the entity type."); } return $this->createBuilder($query); } protected function isBelongsToParentType(): bool { return $this->relationType === Entity::BELONGS_TO_PARENT; } protected function getMapper(): RDBMapper { return $this->entityManager->getMapper(); } /** * Find related records. */ public function find(): Collection { if ($this->isBelongsToParentType()) { $collection = $this->entityManager->getCollectionFactory()->create(); $entity = $this->getMapper()->selectRelated($this->entity, $this->relationName); if ($entity) { $collection[] = $entity; } $collection->setAsFetched(); return $collection; } return $this->createBuilder()->find(); } /** * Find a first record. */ public function findOne(): ?Entity { if ($this->isBelongsToParentType()) { return $this->getMapper()->selectRelated($this->entity, $this->relationName); } $collection = $this->sth()->limit(0, 1)->find(); foreach ($collection as $entity) { return $entity; } return null; } /** * Get a number of related records. */ public function count(): int { return $this->createBuilder()->count(); } /** * Add JOIN. * * @see Espo\ORM\QueryParams\SelectBuilder::join() */ public function join(string $relationName, ?string $alias = null, ?array $conditions = null): Builder { return $this->createBuilder()->join($relationName, $alias, $conditions); } /** * Add LEFT JOIN. * * @see Espo\ORM\QueryParams\SelectBuilder::leftJoin() */ public function leftJoin(string $relationName, ?string $alias = null, ?array $conditions = null): Builder { return $this->createBuilder()->leftJoin($relationName, $alias, $conditions); } /** * Set DISTINCT parameter. */ public function distinct(): Builder { return $this->createBuilder()->distinct(); } /** * Set to return STH collection. Recommended for fetching large number of records. */ public function sth(): Builder { return $this->createBuilder()->sth(); } /** * Add a WHERE clause. * * @see Espo\ORM\QueryParams\SelectBuilder::where() * * @param array|string $keyOrClause * @param ?array|string $value */ public function where($keyOrClause = [], $value = null): Builder { return $this->createBuilder()->where($keyOrClause, $value); } /** * Add a HAVING clause. * * @see Espo\ORM\QueryParams\SelectBuilder::having() * * @param array|string $keyOrClause * @param ?array|string $value */ public function having($keyOrClause = [], $value = null): Builder { return $this->createBuilder()->having($keyOrClause, $value); } /** * Apply ORDER. * * @see Espo\ORM\QueryParams\SelectBuilder::order() * * @param string|int|array $orderBy * @param bool|string $direction */ public function order($orderBy, $direction = 'ASC'): Builder { return $this->createBuilder()->order($orderBy, $direction); } /** * Apply OFFSET and LIMIT. */ public function limit(?int $offset = null, ?int $limit = null): Builder { return $this->createBuilder()->limit($offset, $limit); } /** * Specify SELECT. Which attributes to select. All attributes are selected by default. * * @see Espo\ORM\QueryParams\SelectBuilder::select() * * @param array|string $select */ public function select($select, ?string $alias = null): Builder { return $this->createBuilder()->select($select, $alias); } /** * Specify GROUP BY. */ public function groupBy(array $groupBy): Builder { return $this->createBuilder()->groupBy($groupBy); } /** * Apply middle table conditions for a many-to-many relationship. * * @see Espo\ORM\Repository\RDBRelationSelectBuilder::columnsWhere() */ public function columnsWhere(array $where): Builder { return $this->createBuilder()->columnsWhere($where); } protected function processCheckForeignEntity(Entity $entity): void { if ($this->foreignEntityType && $this->foreignEntityType !== $entity->getEntityType()) { throw new RuntimeException("Entity type doesn't match an entity type of the relation."); } if (!$entity->id) { throw new RuntimeException("Can't use an entity w/o ID."); } } /** * Whether related with an entity. * * @throws RuntimeException */ public function isRelated(Entity $entity): bool { if (!$entity->id) { throw new RuntimeException("Can't use an entity w/o ID."); } if ($this->isBelongsToParentType()) { return $this->isRelatedBelongsToParent($entity); } if ($this->relationType === Entity::BELONGS_TO) { return $this->isRelatedBelongsTo($entity); } $this->processCheckForeignEntity($entity); return (bool) $this->createBuilder() ->select(['id']) ->where(['id' => $entity->id]) ->findOne(); } protected function isRelatedBelongsToParent(Entity $entity): bool { $fromEntity = $this->entity; $idAttribute = $this->relationName . 'Id'; $typeAttribute = $this->relationName . 'Type'; if (!$fromEntity->has($idAttribute) || !$fromEntity->has($typeAttribute)) { $fromEntity = $this->entityManager->getEntity($fromEntity->getEntityType(), $fromEntity->id); } if (!$fromEntity) { return false; } return $fromEntity->get($idAttribute) === $entity->id && $fromEntity->get($typeAttribute) === $entity->getEntityType(); } protected function isRelatedBelongsTo(Entity $entity): bool { $fromEntity = $this->entity; $idAttribute = $this->relationName . 'Id'; if (!$fromEntity->has($idAttribute)) { $fromEntity = $this->entityManager->getEntity($fromEntity->getEntityType(), $fromEntity->id); } if (!$fromEntity) { return false; } return $fromEntity->get($idAttribute) === $entity->id; } /** * Relate with an entity by ID. */ public function relateById(string $id, ?array $columnData = null, array $options = []): void { if ($this->isBelongsToParentType()) { throw new RuntimeException("Can't relate 'belongToParent'."); } if ($id === '') { throw new RuntimeException(); } $seed = $this->entityManager->getEntityFactory()->create($this->foreignEntityType); $seed->set('id', $id); $this->relate($seed, $columnData, $options); } /** * Unrelate from an entity by ID. */ public function unrelateById(string $id, array $options = []): void { if ($this->isBelongsToParentType()) { throw new RuntimeException("Can't unrelate 'belongToParent'."); } if ($id === '') { throw new RuntimeException(); } $seed = $this->entityManager->getEntityFactory()->create($this->foreignEntityType); $seed->set('id', $id); $this->unrelate($seed, $options); } /** * Update relationship columns by ID. For many-to-many relationships. */ public function updateColumnsById(string $id, array $columnData): void { if ($this->isBelongsToParentType()) { throw new RuntimeException("Can't update columns by ID 'belongToParent'."); } if ($id === '') { throw new RuntimeException(); } $seed = $this->entityManager->getEntityFactory()->create($this->foreignEntityType); $seed->set('id', $id); $this->updateColumns($seed, $columnData); } /** * Relate with an entity. */ public function relate(Entity $entity, ?array $columnData = null, array $options = []): void { $this->processCheckForeignEntity($entity); $this->beforeRelate($entity, $columnData, $options); $result = $this->getMapper()->relate($this->entity, $this->relationName, $entity, $columnData); if (!$result) { return; } $this->afterRelate($entity, $columnData, $options); } /** * Unrelate from an entity. */ public function unrelate(Entity $entity, array $options = []): void { $this->processCheckForeignEntity($entity); $this->beforeUnrelate($entity, $options); $this->getMapper()->unrelate($this->entity, $this->relationName, $entity); $this->afterUnrelate($entity, $options); } public function massRelate(Select $query, array $options = []): void { if ($this->isBelongsToParentType()) { throw new RuntimeException("Can't mass relate 'belongToParent'."); } if ($query->getFrom() !== $this->foreignEntityType) { throw new RuntimeException("Passed query doesn't match foreign entity type."); } $this->beforeMassRelate($query, $options); $this->getMapper()->massRelate($this->entity, $this->relationName, $query); $this->afterMassRelate($query, $options); } /** * Update relationship columns. For many-to-many relationships. */ public function updateColumns(Entity $entity, array $columnData): void { $this->processCheckForeignEntity($entity); if ($this->relationType !== Entity::MANY_MANY) { throw new RuntimeException("Can't update not many-to-many relation."); } $this->getMapper()->updateRelationColumns($this->entity, $this->relationName, $entity->getId(), $columnData); } /** * Get a relationship column value. For many-to-many relationships. * * @return mixed */ public function getColumn(Entity $entity, string $column) { $this->processCheckForeignEntity($entity); if ($this->relationType !== Entity::MANY_MANY) { throw new RuntimeException("Can't get a column of not many-to-many relation."); } return $this->getMapper()->getRelationColumn($this->entity, $this->relationName, $entity->getId(), $column); } protected function beforeRelate(Entity $entity, ?array $columnData, array $options): void { $this->hookMediator->beforeRelate($this->entity, $this->relationName, $entity, $columnData, $options); } protected function afterRelate(Entity $entity, ?array $columnData, array $options): void { $this->hookMediator->afterRelate($this->entity, $this->relationName, $entity, $columnData, $options); } protected function beforeUnrelate(Entity $entity, array $options): void { $this->hookMediator->beforeUnrelate($this->entity, $this->relationName, $entity, $options); } protected function afterUnrelate(Entity $entity, array $options): void { $this->hookMediator->afterUnrelate($this->entity, $this->relationName, $entity, $options); } protected function beforeMassRelate(Select $query, array $options): void { $this->hookMediator->beforeMassRelate($this->entity, $this->relationName, $query, $options); } protected function afterMassRelate(Select $query, array $options): void { $this->hookMediator->afterMassRelate($this->entity, $this->relationName, $query, $options); } }