Parser.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. namespace Component\Collection\Util;
  20. use App\Core\Event;
  21. use App\Entity\Actor;
  22. use App\Util\Exception\ServerException;
  23. use Doctrine\Common\Collections\Criteria;
  24. abstract class Parser
  25. {
  26. /**
  27. * Merge $parts into $criteria_arr
  28. */
  29. private static function connectParts(array &$parts, array &$criteria_arr, string $last_op, mixed $eb, bool $force = false): void
  30. {
  31. foreach ([' ' => 'orX', '|' => 'orX', '&' => 'andX'] as $op => $func) {
  32. if ($last_op === $op || $force) {
  33. $criteria_arr[] = $eb->{$func}(...$parts);
  34. break;
  35. }
  36. }
  37. }
  38. /**
  39. * Parse $input string into a Doctrine query Criteria
  40. *
  41. * Currently doesn't support nesting with parenthesis and
  42. * recognises either spaces (currently `or`, should be fuzzy match), `OR` or `|` (`or`) and `AND` or `&` (`and`)
  43. *
  44. * TODO: Better fuzzy match, implement exact match with quotes and nesting with parens
  45. * TODO: Proper parser, tokenize better. Mostly a rewrite
  46. *
  47. * @return array{?Criteria, ?Criteria} [?$note_criteria, ?$actor_criteria]
  48. */
  49. public static function parse(string $input, ?string $locale = null, ?Actor $actor = null, int $level = 0): array
  50. {
  51. if ($level === 0) {
  52. $input = trim(preg_replace(['/\s+/', '/\s+AND\s+/', '/\s+OR\s+/'], [' ', '&', '|'], $input), ' |&');
  53. }
  54. $left = 0;
  55. $right = 0;
  56. $lenght = mb_strlen($input);
  57. $eb = Criteria::expr();
  58. $note_criteria_arr = [];
  59. $actor_criteria_arr = [];
  60. $note_parts = [];
  61. $actor_parts = [];
  62. $last_op = null;
  63. for ($index = 0; $index < $lenght; ++$index) {
  64. $end = false;
  65. $match = false;
  66. foreach (['&', '|', ' '] as $delimiter) {
  67. if ($input[$index] === $delimiter || $end = ($index === $lenght - 1)) {
  68. $term = mb_substr($input, $left, $end ? null : $right - $left);
  69. $note_res = null;
  70. $actor_res = null;
  71. Event::handle('CollectionQueryCreateExpression', [$eb, $term, $locale, $actor, &$note_res, &$actor_res]);
  72. if (\is_null($note_res) && \is_null($actor_res)) { // @phpstan-ignore-line
  73. //throw new ServerException("No one claimed responsibility for a match term: {$term}");
  74. // It's okay if the term doesn't exist, just perform a regular search
  75. }
  76. if (!empty($note_res)) { // @phpstan-ignore-line currently an open bug. See https://web.archive.org/web/20220226131651/https://github.com/phpstan/phpstan/issues/6234
  77. if (\is_array($note_res)) {
  78. $note_res = $eb->orX(...$note_res);
  79. }
  80. $note_parts[] = $note_res;
  81. }
  82. if (!empty($actor_res)) { // @phpstan-ignore-line currently an open bug. See https://web.archive.org/web/20220226131651/https://github.com/phpstan/phpstan/issues/6234
  83. if (\is_array($actor_res)) {
  84. $actor_res = $eb->orX(...$actor_res);
  85. }
  86. $actor_parts[] = $actor_res;
  87. }
  88. $right = $left = $index + 1;
  89. if (!\is_null($last_op) && $last_op !== $delimiter) {
  90. self::connectParts($note_parts, $note_criteria_arr, $last_op, $eb, force: false);
  91. } else {
  92. $last_op = $delimiter;
  93. }
  94. $match = true;
  95. break;
  96. }
  97. }
  98. // TODO
  99. if (!$match) {
  100. ++$right;
  101. }
  102. }
  103. $note_criteria = null;
  104. $actor_criteria = null;
  105. if (!empty($note_parts)) {
  106. self::connectParts($note_parts, $note_criteria_arr, $last_op, $eb, force: true);
  107. $note_criteria = new Criteria($eb->orX(...$note_criteria_arr));
  108. }
  109. if (!empty($actor_parts)) { // @phpstan-ignore-line weird, but this whole thing needs a rewrite
  110. self::connectParts($actor_parts, $actor_criteria_arr, $last_op, $eb, force: true);
  111. $actor_criteria = new Criteria($eb->orX(...$actor_criteria_arr));
  112. }
  113. return [$note_criteria, $actor_criteria];
  114. }
  115. }