| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- <?php
- namespace PhpDevCommunity\PaperORM\Query;
- use InvalidArgumentException;
- use LogicException;
- use PhpDevCommunity\PaperORM\Cache\EntityMemcachedCache;
- use PhpDevCommunity\PaperORM\EntityManagerInterface;
- use PhpDevCommunity\PaperORM\Hydrator\ArrayHydrator;
- use PhpDevCommunity\PaperORM\Hydrator\EntityHydrator;
- use PhpDevCommunity\PaperORM\Hydrator\ReadOnlyEntityHydrator;
- use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
- use PhpDevCommunity\PaperORM\Mapper\EntityMapper;
- use PhpDevCommunity\PaperORM\Mapping\Column\Column;
- use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
- use PhpDevCommunity\PaperORM\Mapping\OneToMany;
- use PhpDevCommunity\PaperORM\Platform\PlatformInterface;
- use PhpDevCommunity\PaperORM\Schema\SchemaInterface;
- use PhpDevCommunity\Sql\QL\JoinQL;
- final class QueryBuilder
- {
- public const HYDRATE_OBJECT = 'object';
- public const HYDRATE_OBJECT_READONLY = 'readonly';
- public const HYDRATE_ARRAY = 'array';
- private PlatformInterface $platform;
- private SchemaInterface $schema;
- private EntityMemcachedCache $cache;
- private AliasGenerator $aliasGenerator;
- private array $select = [];
- private array $where = [];
- private array $orderBy = [];
- private array $joins = [];
- private array $joinsAlreadyAdded = [];
- private ?int $firstResult = null;
- private ?int $maxResults = null;
- private array $params = [];
- public function __construct(EntityManagerInterface $em)
- {
- $this->platform = $em->getPlatform();
- $this->schema = $this->platform->getSchema();;
- $this->cache = $em->getCache();
- $this->aliasGenerator = new AliasGenerator();
- }
- public function getResultIterator(string $hydrationMode = self::HYDRATE_OBJECT): iterable
- {
- foreach ($this->buildSqlQuery()->getResultIterator() as $item) {
- yield $this->hydrate([$item], $hydrationMode)[0];
- }
- }
- public function getResult(string $hydrationMode = self::HYDRATE_OBJECT): array
- {
- return $this->hydrate($this->buildSqlQuery()->getResult(), $hydrationMode);
- }
- public function getOneOrNullResult(string $hydrationMode = self::HYDRATE_OBJECT)
- {
- $item = $this->buildSqlQuery()->getOneOrNullResult();
- if ($item === null) {
- return null;
- }
- return $this->hydrate([$item], $hydrationMode)[0];
- }
- public function getCountResult(): int
- {
- return $this->buildSqlQuery()->count();
- }
- public function select(string $entityName, array $properties = []): self
- {
- $this->select = [
- 'table' => $this->getTableName($entityName),
- 'entityName' => $entityName,
- 'alias' => $this->aliasGenerator->generateAlias($entityName),
- 'properties' => $properties
- ];
- return $this;
- }
- public function getPrimaryAlias(): string
- {
- if (empty($this->select)) {
- throw new LogicException('Select must be called before getPrimaryAlias');
- }
- return $this->select['alias'];
- }
- public function getPrimaryEntityName(): string
- {
- if (empty($this->select)) {
- throw new LogicException('Select must be called before getPrimaryEntityName');
- }
- return $this->select['entityName'];
- }
- public function where(string ...$expressions): self
- {
- foreach ($expressions as $expression) {
- $this->where[] = $expression;
- }
- return $this;
- }
- public function orderBy(string $sort, string $order = 'ASC'): self
- {
- $this->orderBy[] = [
- 'sort' => $sort,
- 'order' => $order
- ];
- return $this;
- }
- public function setParams(array $params): self
- {
- $this->params = $params;
- return $this;
- }
- public function setParam(string $name, $value): self
- {
- $this->params[$name] = $value;
- return $this;
- }
- public function resetWhere(): self
- {
- $this->where = [];
- return $this;
- }
- public function resetOrderBy() : self
- {
- $this->orderBy = [];
- return $this;
- }
- public function resetParams(): self
- {
- $this->params = [];
- return $this;
- }
- public function leftJoin(string $fromAliasOrEntityName, string $targetEntityName, ?string $property = null): self
- {
- return $this->join('LEFT', $fromAliasOrEntityName, $targetEntityName, $property);
- }
- public function innerJoin(string $fromAliasOrEntityName, string $targetEntityName, ?string $property = null): self
- {
- return $this->join('INNER', $fromAliasOrEntityName, $targetEntityName, $property);
- }
- public function setFirstResult(?int $firstResult): self
- {
- if ($this->select === []) {
- throw new LogicException(
- 'You must call the select() method first to define the main table for the query '
- );
- }
- $this->firstResult = $firstResult;
- return $this;
- }
- public function setMaxResults(?int $maxResults): self
- {
- if ($this->select === []) {
- throw new LogicException(
- 'You must call the select() method first to define the main table for the query '
- );
- }
- $this->maxResults = $maxResults;
- return $this;
- }
- private function join(string $type, string $fromAliasOrEntityName, string $targetEntityName, ?string $property = null): self
- {
- if (class_exists($fromAliasOrEntityName)) {
- $fromAliases = $this->getAliasesFromEntityName($fromAliasOrEntityName);
- } else {
- $fromAliases = [$fromAliasOrEntityName];
- }
- /**
- * @comment IS security , we need to check if the join is already added !!!
- */
- $key = md5(sprintf('%s.%s.%s.%s', $type,$fromAliasOrEntityName, $targetEntityName, $property));
- if (in_array($key, $this->joinsAlreadyAdded)) {
- return $this;
- }
- $this->joinsAlreadyAdded[] = $key;
- foreach ($fromAliases as $fromAlias) {
- $fromEntityName = $this->getEntityNameFromAlias($fromAlias);
- $columns = $this->getRelationsColumns($fromEntityName, $targetEntityName, $property);
- foreach ($columns as $column) {
- $alias = $this->aliasGenerator->generateAlias($targetEntityName);
- $this->joins[$alias] = [
- 'type' => $type,
- 'alias' => $alias,
- 'targetEntity' => $targetEntityName,
- 'targetTable' => $this->getTableName($targetEntityName),
- 'fromEntityName' => $fromEntityName,
- 'fromTable' => $this->getTableName($fromEntityName),
- 'fromAlias' => $fromAlias,
- 'column' => $column,
- 'property' => $property,
- 'isOneToMany' => $column instanceof OneToMany
- ];
- }
- }
- return $this;
- }
- private function convertPropertiesToColumns(string $entityName, array $properties): array
- {
- $columns = [];
- foreach ($properties as $property) {
- if ($property instanceof Column) {
- $propertyName = $property->getProperty();
- } elseif (is_string($property)) {
- $propertyName = $property;
- } else {
- throw new InvalidArgumentException("Property {$property} not found in class " . $entityName);
- }
- $column = ColumnMapper::getColumnByProperty($entityName, $propertyName);
- if ($column === null) {
- throw new InvalidArgumentException("Property {$propertyName} not found in class " . $entityName);
- }
- $columns[] = $this->schema->quote($column->getName());
- }
- return $columns;
- }
- /**
- * @param string $entityName
- * @param string $targetEntityName
- * @param string|null $property
- * @return array<int,JoinColumn|OneToMany>
- */
- private function getRelationsColumns(string $entityName, string $targetEntityName, ?string $property = null): array
- {
- $relationsColumns = [];
- foreach (ColumnMapper::getColumns($entityName) as $column) {
- if ($column instanceof JoinColumn) {
- $relationsColumns[$column->getProperty()] = $column;
- }
- }
- foreach (ColumnMapper::getOneToManyRelations($entityName) as $column) {
- $relationsColumns[$column->getProperty()] = $column;
- }
- if ($relationsColumns === []) {
- throw new InvalidArgumentException("Entity {$targetEntityName} not found in class " . $entityName);
- }
- $columns = [];
- if ($property) {
- $column = $relationsColumns[$property] ?? null;
- if ($column) {
- $columns[] = $column;
- }
- } else {
- foreach ($relationsColumns as $column) {
- if ($column->getTargetEntity() === $targetEntityName) {
- $columns[] = $column;
- }
- }
- }
- if ($columns === []) {
- throw new InvalidArgumentException("Entity {$targetEntityName} not found in class " . $entityName);
- }
- return $columns;
- }
- private function getTableName(string $entityName): string
- {
- return EntityMapper::getTable($entityName);
- }
- public function buildSqlQuery(): JoinQL
- {
- if ($this->select === []) {
- throw new LogicException('No query specified');
- }
- $properties = $this->select['properties'];
- $entityName = $this->select['entityName'];
- $alias = $this->select['alias'];
- $table = $this->select['table'];
- if ($properties === []) {
- $properties = ColumnMapper::getColumns($entityName);
- }
- $columns = $this->convertPropertiesToColumns($entityName, $properties);
- $primaryKey = ColumnMapper::getPrimaryKeyColumnName($entityName);
- $primaryKeyQuoted = $this->schema->quote($primaryKey);
- if (!in_array($primaryKeyQuoted, $columns)) {
- $columns[] = $primaryKeyQuoted;
- }
- if ($columns[0] !== $primaryKeyQuoted) {
- $columns = array_unique([$primaryKeyQuoted, ...$columns]);
- }
- $joinQl = new JoinQL($this->platform->getConnection()->getPdo(), $primaryKey);
- $joinQl->select($this->schema->quote($table), $alias, $columns);
- foreach ($this->joins as $join) {
- $fromAlias = $join['fromAlias'];
- $targetTable = $join['targetTable'];
- $targetEntity = $join['targetEntity'];
- $alias = $join['alias'];
- /**
- * @var JoinColumn|OneToMany $column
- */
- $column = $join['column'];
- $isOneToMany = $join['isOneToMany'];
- $type = $join['type'];
- $name = null;
- $columns = $this->convertPropertiesToColumns($targetEntity, ColumnMapper::getColumns($targetEntity));
- $joinQl->addSelect($alias, $columns);
- $criteria = [];
- if ($column instanceof JoinColumn) {
- $criteria = [$column->getName() => $column->getReferencedColumnName()];
- $name = $column->getName();
- } elseif ($column instanceof OneToMany) {
- $criteria = $column->getCriteria();
- $mappedBy = $column->getMappedBy(); //@todo VOIR SI RENDRE OBLIGATOIRE : A DISCUTER !!!
- if ($mappedBy) {
- $columnMappedBy = ColumnMapper::getColumnByProperty($targetEntity, $mappedBy);
- if (!$columnMappedBy instanceof JoinColumn) {
- throw new InvalidArgumentException("Property mapped by {$mappedBy} not found in class " . $targetEntity);
- }
- $name = $columnMappedBy->getName();
- $criteria = $criteria + [$columnMappedBy->getReferencedColumnName() => $columnMappedBy->getName()];
- }
- }
- $joinConditions = [];
- $targetTable = $this->schema->quote($targetTable);
- foreach ($criteria as $key => $value) {
- $value = "$alias.$value";
- $joinConditions[] = "$fromAlias.$key = $value";
- }
- if ($type === 'LEFT') {
- $joinQl->leftJoin($fromAlias, $targetTable, $alias, $joinConditions, $isOneToMany, $column->getProperty(), $name);
- } elseif ($type === 'INNER') {
- $joinQl->innerJoin($fromAlias, $targetTable, $alias, $joinConditions, $isOneToMany, $column->getProperty(), $name);
- }
- }
- foreach ($this->where as $where) {
- $joinQl->where($this->resolveExpression($where));
- }
- foreach ($this->orderBy as $orderBy) {
- $joinQl->orderBy($this->resolveExpression($orderBy['sort']), $orderBy['order']);
- }
- foreach ($this->params as $key => $value) {
- $joinQl->setParam($key, $value);
- }
- if ($this->maxResults) {
- $joinQl->setMaxResults($this->maxResults);
- }
- if ($this->firstResult) {
- $joinQl->setFirstResult($this->firstResult);
- }
- return $joinQl;
- }
- public function getAliasesFromEntityName(string $entityName, string $property = null): array
- {
- $aliases = [];
- if (isset($this->select['entityName']) && $this->select['entityName'] === $entityName) {
- $aliases[] = $this->select['alias'];
- }
- foreach ($this->joins as $keyAsAlias => $join) {
- if ($join['targetEntity'] === $entityName && $join['property'] === $property) {
- $aliases[] = $keyAsAlias;
- }
- }
- if ($aliases === []) {
- throw new LogicException('Alias not found for ' . $entityName);
- }
- return $aliases;
- }
- private function getEntityNameFromAlias(string $alias): string
- {
- if (isset($this->select['alias']) && $this->select['alias'] === $alias) {
- return $this->select['entityName'];
- }
- $entityName = null;
- foreach ($this->joins as $keyAsAlias => $join) {
- if ($keyAsAlias === $alias) {
- $entityName = $join['targetEntity'];
- break;
- }
- }
- if ($entityName === null) {
- throw new LogicException('Entity name not found for ' . $alias);
- }
- return $entityName;
- }
- private function hydrate(array $data, string $hydrationMode): array
- {
- if ($hydrationMode === self::HYDRATE_OBJECT) {
- $hydrator = new EntityHydrator($this->cache);
- } elseif ($hydrationMode === self::HYDRATE_OBJECT_READONLY) {
- $hydrator = new ReadOnlyEntityHydrator();
- } else {
- $hydrator = new ArrayHydrator();
- }
- $collection = [];
- foreach ($data as $item) {
- $collection[] = $hydrator->hydrate($this->select['entityName'], $item);
- }
- return $collection;
- }
- private function resolveExpression(string $expression): string
- {
- $aliases = AliasDetector::detect($expression);
- foreach ($aliases as $alias => $properties) {
- $fromEntityName = $this->getEntityNameFromAlias($alias);
- foreach ($properties as $property) {
- $column = ColumnMapper::getColumnByProperty($fromEntityName, $property);
- if ($column === null) {
- throw new InvalidArgumentException(sprintf('Property %s not found in class %s or is a collection and cannot be used in an expression', $property, $fromEntityName));
- }
- $expression = str_replace($alias . '.' . $property, $this->schema->quote($alias) . '.'.$this->schema->quote($column->getName()), $expression);
- }
- }
- return $expression;
- }
- }
|