From 058900e4ece57030d511eb726f196008e7f66cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Proch=C3=A1zka?= Date: Mon, 5 Dec 2022 10:15:26 +0100 Subject: [PATCH] Added code moved from juniwalk/utils:^1.2.38 --- composer.json | 25 +++ src/AbstractRepository.php | 176 +++++++++++++++++++++ src/Exceptions/EntityNotFoundException.php | 22 +++ src/Exceptions/EntityNotValidException.php | 22 +++ src/Functions/Cast.php | 95 +++++++++++ src/Functions/DatePart.php | 47 ++++++ src/Functions/DateTrunc.php | 47 ++++++ src/Functions/Floor.php | 42 +++++ src/Functions/JsonStr.php | 47 ++++++ src/Functions/Replace.php | 50 ++++++ src/Functions/ToChar.php | 47 ++++++ src/Functions/Unaccent.php | 42 +++++ src/Interfaces/HtmlOption.php | 15 ++ src/Traits/Activable.php | 28 ++++ src/Traits/Hashable.php | 36 +++++ src/Traits/Identifier.php | 30 ++++ src/Traits/Parametrized.php | 61 +++++++ src/Traits/Timestamp.php | 63 ++++++++ src/Utils/NetteEntityListenerResolver.php | 51 ++++++ src/Utils/SortableNullsWalker.php | 65 ++++++++ 20 files changed, 1011 insertions(+) create mode 100644 composer.json create mode 100644 src/AbstractRepository.php create mode 100644 src/Exceptions/EntityNotFoundException.php create mode 100644 src/Exceptions/EntityNotValidException.php create mode 100644 src/Functions/Cast.php create mode 100644 src/Functions/DatePart.php create mode 100644 src/Functions/DateTrunc.php create mode 100644 src/Functions/Floor.php create mode 100644 src/Functions/JsonStr.php create mode 100644 src/Functions/Replace.php create mode 100644 src/Functions/ToChar.php create mode 100644 src/Functions/Unaccent.php create mode 100644 src/Interfaces/HtmlOption.php create mode 100644 src/Traits/Activable.php create mode 100644 src/Traits/Hashable.php create mode 100644 src/Traits/Identifier.php create mode 100644 src/Traits/Parametrized.php create mode 100644 src/Traits/Timestamp.php create mode 100644 src/Utils/NetteEntityListenerResolver.php create mode 100644 src/Utils/SortableNullsWalker.php diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e36acc3 --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "juniwalk/orm", + "description": "Nice to have tools for doctrine/orm in nette framework.", + "type": "library", + "keywords": ["nette","nettefw","doctrine","orm"], + "license": "MIT", + + "authors": [{ + "name": "Martin Procházka", + "email": "juniwalk@outlook.cz", + "role": "Developer" + }], + + "autoload": { + "psr-4": {"JuniWalk\\ORM\\": "src/"} + }, + + "require": { + "php": ">=8.1", + "juniwalk/utils": ">=2.0", + "nettrine/migrations": "^0.8", + "nettrine/orm": "^0.8", + "ramsey/uuid": "^4.0" + } +} diff --git a/src/AbstractRepository.php b/src/AbstractRepository.php new file mode 100644 index 0000000..7b195f5 --- /dev/null +++ b/src/AbstractRepository.php @@ -0,0 +1,176 @@ +connection = $entityManager->getConnection(); + $this->entityManager = $entityManager; + + if (!$this->entityName) { + throw EntityNotFoundException::fromClass($this->entityName); + } + } + + + /** + * @throws NoResultException + */ + public function getBy(callable $where, ?int $maxResults = null): array + { + /** @see Hardcoded indexBy might cause issues with entities without $id */ + $qb = $this->createQueryBuilder('e', 'e.id', $where); + $qb->setMaxResults($qb->getMaxResults() ?? $maxResults); + + return $qb->getQuery() + ->getResult(); + } + + + /** + * @throws NoResultException + */ + public function getOneBy(callable $where): object + { + /** @see Hardcoded indexBy might cause issues with entities without $id */ + $qb = $this->createQueryBuilder('e', 'e.id', $where); + $qb->setMaxResults(1); + + return $qb->getQuery() + ->getSingleResult(); + } + + + public function findBy(callable $where, ?int $maxResults = null): array + { + try { + return $this->getBy($where, $maxResults); + + } catch (NoResultException) { + return []; + } + } + + + public function findOneBy(callable $where): ?object + { + try { + return $this->getOneBy($where); + + } catch (NoResultException) { + return null; + } + } + + + /** + * @throws NoResultException + */ + public function getById(int $id): object + { + return $this->getOneBy(function($qb) use ($id) { + $qb->where('e.id = :id')->setParameter('id', $id); + }); + } + + + public function findById(int $id): ?object + { + try { + return $this->getById($id); + + } catch (NoResultException) { + return null; + } + } + + + public function createQueryBuilder(string $alias, string $indexBy = null, callable $where = null): QueryBuilder + { + $qb = $this->entityManager->createQueryBuilder()->select($alias) + ->from($this->entityName, $alias, $indexBy); + + if ($where) { + $qb = $where($qb) ?: $qb; + } + + return $qb; + } + + + public function createQuery(string $dql = null): Query + { + return $this->entityManager->createQuery($dql); + } + + + public function getReference(?int $id, string $entityName = null): ?object + { + if (!$id || empty($id)) { + return null; + } + + return $this->entityManager->getReference($entityName ?: $this->entityName, $id); + } + + + public function getFormReference(string $field, Form $form): ?object + { + return $this->getReference( + (int) $form->getHttpData($form::DATA_LINE, $field) ?: null + ); + } + + + public function truncateTable(bool $cascade = false, string $entityName = null): void + { + $this->query('TRUNCATE TABLE "'.$this->getTableName($entityName).'" RESTART IDENTITY'.($cascade == true ? ' CASCADE' : null)); + } + + + public function getTableName(string $entityName = null): string + { + $entityName = $entityName ?: $this->entityName; + $metaData = $this->entityManager->getClassMetadata($entityName); + $tableName = $metaData->getTableName(); + + if ($schemaName = $metaData->getSchemaName()) { + $tableName = $schemaName.'.'.$tableName; + } + + return $tableName; + } + + + /** + * @throws DBALException + */ + private function query(string $query): mixed + { + return $this->connection->query($query); + } +} diff --git a/src/Exceptions/EntityNotFoundException.php b/src/Exceptions/EntityNotFoundException.php new file mode 100644 index 0000000..ecd2e78 --- /dev/null +++ b/src/Exceptions/EntityNotFoundException.php @@ -0,0 +1,22 @@ +match(Lexer::T_IDENTIFIER); // (2) + $parser->match(Lexer::T_OPEN_PARENTHESIS); // (3) + $lexer = $parser->getLexer(); + + $this->column = $parser->StringPrimary(); // (4) + + switch (true) { + case $lexer->isNextToken(Lexer::T_COMMA): + $parser->match(Lexer::T_COMMA); // (5) + break; + case $lexer->isNextToken(Lexer::T_AS): + $parser->match(Lexer::T_AS); // (5) + break; + } + + switch (true) { + case $lexer->isNextToken(Lexer::T_STRING): + $this->type = $parser->ScalarExpression(); // (6) + break; + case $lexer->isNextToken(Lexer::T_IDENTIFIER): + $parser->match(Lexer::T_IDENTIFIER); + $this->type = new Literal(Literal::STRING, $lexer->token['value']); // (6) + break; + } + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); // (7) + } + + + /** + * @see https://github.com/oroinc/doctrine-extensions/blob/master/src/Oro/ORM/Query/AST/Platform/Functions/Postgresql/Cast.php + */ + public function getSql(SqlWalker $sqlWalker): string + { + /** @var Node $value */ + $column = $sqlWalker->walkSimpleArithmeticExpression($this->column); + $type = $sqlWalker->walkSimpleArithmeticExpression($this->type); + + $type = trim(strtolower($type), '"\''); + if ($type === 'datetime') { + return '"timestamp"(' . $column . ')'; + } + + if ($type === 'json' && !$sqlWalker->getConnection()->getDatabasePlatform()->hasNativeJsonType()) { + $type = 'text'; + } + + if ($type === 'bool') { + $type = 'boolean'; + } + + if ($type === 'binary') { + $type = 'bytea'; + } + + /** + * The notations varchar(n) and char(n) are aliases for character varying(n) and character(n), respectively. + * character without length specifier is equivalent to character(1). If character varying is used + * without length specifier, the type accepts strings of any size. The latter is a PostgreSQL extension. + * http://www.postgresql.org/docs/9.2/static/datatype-character.html + */ + if ($type === 'string') { + $type = 'varchar'; + } + + return 'cast(' . $column . ' AS ' . $type . ')'; + } +} diff --git a/src/Functions/DatePart.php b/src/Functions/DatePart.php new file mode 100644 index 0000000..3a45408 --- /dev/null +++ b/src/Functions/DatePart.php @@ -0,0 +1,47 @@ +match(Lexer::T_IDENTIFIER); // (2) + $parser->match(Lexer::T_OPEN_PARENTHESIS); // (3) + + $this->interval = $parser->StringPrimary(); // (4) + + $parser->match(Lexer::T_COMMA); // (5) + + $this->column = $parser->StringPrimary(); // (6) + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); // (7) + } + + + public function getSql(SqlWalker $sqlWalker): string + { + $interval = $sqlWalker->walkSimpleArithmeticExpression($this->interval); + $column = $sqlWalker->walkSimpleArithmeticExpression($this->column); + + return 'date_part('.$interval.', '.$column.')'; + } +} diff --git a/src/Functions/DateTrunc.php b/src/Functions/DateTrunc.php new file mode 100644 index 0000000..cc0d14d --- /dev/null +++ b/src/Functions/DateTrunc.php @@ -0,0 +1,47 @@ +match(Lexer::T_IDENTIFIER); // (2) + $parser->match(Lexer::T_OPEN_PARENTHESIS); // (3) + + $this->interval = $parser->StringPrimary(); // (4) + + $parser->match(Lexer::T_COMMA); // (5) + + $this->column = $parser->StringPrimary(); // (6) + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); // (7) + } + + + public function getSql(SqlWalker $sqlWalker): string + { + $interval = $sqlWalker->walkSimpleArithmeticExpression($this->interval); + $column = $sqlWalker->walkSimpleArithmeticExpression($this->column); + + return 'date_trunc('.$interval.', '.$column.')'; + } +} diff --git a/src/Functions/Floor.php b/src/Functions/Floor.php new file mode 100644 index 0000000..ec970ab --- /dev/null +++ b/src/Functions/Floor.php @@ -0,0 +1,42 @@ +match(Lexer::T_IDENTIFIER); // (2) + $parser->match(Lexer::T_OPEN_PARENTHESIS); // (3) + + $this->column = $parser->SimpleArithmeticExpression(); // (4) + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); // (3) + } + + + public function getSql(SqlWalker $sqlWalker): string + { + $column = $sqlWalker->walkSimpleArithmeticExpression($this->column); + + return 'floor('.$column.')'; + } +} diff --git a/src/Functions/JsonStr.php b/src/Functions/JsonStr.php new file mode 100644 index 0000000..f37f62a --- /dev/null +++ b/src/Functions/JsonStr.php @@ -0,0 +1,47 @@ +match(Lexer::T_IDENTIFIER); // (2) + $parser->match(Lexer::T_OPEN_PARENTHESIS); // (3) + + $this->column = $parser->StringPrimary(); // (4) + + $parser->match(Lexer::T_COMMA); // (5) + + $this->property = $parser->SimpleArithmeticExpression(); // (6) + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); // (7) + } + + + public function getSql(SqlWalker $sqlWalker): string + { + $property = $sqlWalker->walkSimpleArithmeticExpression($this->property); + $column = $sqlWalker->walkSimpleArithmeticExpression($this->column); + + return "{$column}->>{$property}"; + } +} diff --git a/src/Functions/Replace.php b/src/Functions/Replace.php new file mode 100644 index 0000000..2e3d315 --- /dev/null +++ b/src/Functions/Replace.php @@ -0,0 +1,50 @@ +match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->column = $parser->StringPrimary(); + + $parser->match(Lexer::T_COMMA); + + $this->from = $parser->StringPrimary(); + + $parser->match(Lexer::T_COMMA); + + $this->to = $parser->StringPrimary(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } + + + public function getSql(SqlWalker $sqlWalker): string + { + return sprintf('REPLACE(%s, %s, %s)', + $this->column->dispatch($sqlWalker), + $this->from->dispatch($sqlWalker), + $this->to->dispatch($sqlWalker) + ); + } +} diff --git a/src/Functions/ToChar.php b/src/Functions/ToChar.php new file mode 100644 index 0000000..a683bb1 --- /dev/null +++ b/src/Functions/ToChar.php @@ -0,0 +1,47 @@ +match(Lexer::T_IDENTIFIER); // (2) + $parser->match(Lexer::T_OPEN_PARENTHESIS); // (3) + + $this->column = $parser->StringPrimary(); // (4) + + $parser->match(Lexer::T_COMMA); // (5) + + $this->format = $parser->StringPrimary(); // (6) + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); // (7) + } + + + public function getSql(SqlWalker $sqlWalker): string + { + $column = $sqlWalker->walkSimpleArithmeticExpression($this->column); + $format = $sqlWalker->walkSimpleArithmeticExpression($this->format); + + return "to_char($column, $format)"; + } +} diff --git a/src/Functions/Unaccent.php b/src/Functions/Unaccent.php new file mode 100644 index 0000000..b6bcc1d --- /dev/null +++ b/src/Functions/Unaccent.php @@ -0,0 +1,42 @@ +match(Lexer::T_IDENTIFIER); // (2) + $parser->match(Lexer::T_OPEN_PARENTHESIS); // (3) + + $this->column = $parser->StringPrimary(); // (4) + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); // (3) + } + + + public function getSql(SqlWalker $sqlWalker): string + { + $column = $sqlWalker->walkSimpleArithmeticExpression($this->column); + + return 'unaccent('.$column.')'; + } +} diff --git a/src/Interfaces/HtmlOption.php b/src/Interfaces/HtmlOption.php new file mode 100644 index 0000000..53f922d --- /dev/null +++ b/src/Interfaces/HtmlOption.php @@ -0,0 +1,15 @@ + true])] + private bool $isActive = true; + + + public function setActive(bool $active): void + { + $this->isActive = $active; + } + + + public function isActive(): bool + { + return $this->isActive; + } +} diff --git a/src/Traits/Hashable.php b/src/Traits/Hashable.php new file mode 100644 index 0000000..c9dcdbc --- /dev/null +++ b/src/Traits/Hashable.php @@ -0,0 +1,36 @@ +hash ?: $this->createUniqueHash(); + } + + + /** + * @throws InvalidArgumentException + */ + protected function createUniqueHash(): string + { + if (!$this instanceof Stringable) { + throw new InvalidArgumentException('Entity has to implement Stringable or use custom createUniqueHash method'); + } + + return substr(sha1((string) $this), 0, 8); + } +} diff --git a/src/Traits/Identifier.php b/src/Traits/Identifier.php new file mode 100644 index 0000000..76ba0a4 --- /dev/null +++ b/src/Traits/Identifier.php @@ -0,0 +1,30 @@ +id ?? null; + } + + + public function __clone() + { + unset($this->id); + } +} diff --git a/src/Traits/Parametrized.php b/src/Traits/Parametrized.php new file mode 100644 index 0000000..9b0435c --- /dev/null +++ b/src/Traits/Parametrized.php @@ -0,0 +1,61 @@ +hasParam($key)) { + return; + } + + $this->params[$key] = $value; + + if (is_null($value)) { + unset($this->params[$key]); + } + } + + + public function getParam(string $key): mixed + { + if (!$this->hasParam($key)) { + return null; + } + + return $this->params[$key] ?? null; + } + + + public function getParams(): array + { + return $this->params; + } + + + public function hasParam(string $key): bool + { + return isset($this->params[$key]); + } +} diff --git a/src/Traits/Timestamp.php b/src/Traits/Timestamp.php new file mode 100644 index 0000000..17ba250 --- /dev/null +++ b/src/Traits/Timestamp.php @@ -0,0 +1,63 @@ + 'CURRENT_TIMESTAMP'])] + private DateTime $created; + + #[ORM\Column(type: 'datetimetz', nullable: true)] + private ?DateTime $modified = null; + + + public function getCreated(): DateTime + { + return clone $this->created; + } + + + public function setModified(?DateTimeInterface $modified): void + { + $this->modified = $modified ? clone $modified : new DateTime; + } + + + public function getModified(): ?DateTimeInterface + { + if (!$this->modified) { + return null; + } + + return clone $this->modified; + } + + + public function getTimestamp(): DateTimeInterface + { + return clone ($this->modified ?: $this->created); + } + + + #[ORM\PreUpdate] + public function onUpdate(): void + { + $this->modified = new DateTime; + } + + + #[ORM\PrePersist] + public function onPersist(): void + { + $this->created = new DateTime; + } +} diff --git a/src/Utils/NetteEntityListenerResolver.php b/src/Utils/NetteEntityListenerResolver.php new file mode 100644 index 0000000..c70679b --- /dev/null +++ b/src/Utils/NetteEntityListenerResolver.php @@ -0,0 +1,51 @@ +container->removeService($className); + } + + + /** + * @inheritdoc + * @throws MissingServiceException + */ + public function resolve(/*string*/ $className)//: object + { + return $this->container->getByType($className); + } + + + /** + * @inheritdoc + */ + public function register(/*object*/ $resolver)//: void + { + $this->container->addService(get_class($resolver), $resolver); + } +} diff --git a/src/Utils/SortableNullsWalker.php b/src/Utils/SortableNullsWalker.php new file mode 100644 index 0000000..c37e912 --- /dev/null +++ b/src/Utils/SortableNullsWalker.php @@ -0,0 +1,65 @@ +getQuery(); + * $query->setHint(Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, SortableNullsWalker::class); + * $query->setHint(SortableNullsWalker::FIELDS_KEY, [ + * 'p.firstname' => SortableNullsWalker::NULLS_FIRST, + * ]); + */ +class SortableNullsWalker extends SqlWalker +{ + /** @var string */ + public const FIELDS_KEY = 'sortableNulls.fields'; + + /** @var string */ + public const NULLS_FIRST = 'NULLS FIRST'; + public const NULLS_LAST = 'NULLS LAST'; + + + /** + * {@inheritDoc} + */ + public function walkOrderByItem($orderByItem) + { + $hint = $this->getQuery()->getHint(self::FIELDS_KEY); + $sql = parent::walkOrderByItem($orderByItem); + + if (empty($hint) || !is_array($hint)) { + return $sql; + } + + $expr = $orderByItem->expression; + $type = strtoupper($orderByItem->type); + + if (!$expr instanceof PathExpression || $expr->type != PathExpression::TYPE_STATE_FIELD) { + return $sql; + } + + $index = $expr->identificationVariable.'.'.$expr->field; + + if (!isset($hint[$index])) { + return $sql; + } + + $search = $this->walkPathExpression($expr).' '.$type; + $sql = str_replace($search, $search.' '.$hint[$index], $sql); + + return $sql; + } +}