From 06c5a90214e251f6f1e9ea71c0649a3226a5f6d4 Mon Sep 17 00:00:00 2001 From: Matt Feeny Date: Wed, 20 Jul 2022 09:23:11 +0100 Subject: [PATCH] First pass of the search bundle --- .gitignore | 2 + Containerfile | 7 ++ Makefile | 8 ++ README.md | 0 compose.yml | 7 ++ composer.json | 31 +++++++ psalm.xml | 15 ++++ src/Command/SearchIndexCommand.php | 72 +++++++++++++++ src/Entity/Interface/SearchableInterface.php | 11 +++ src/Entity/SearchIndex.php | 81 +++++++++++++++++ src/EventSubscriber/SearchableSubscriber.php | 52 +++++++++++ src/PcmSearchBundle.php | 12 +++ src/Service/SearchService.php | 94 ++++++++++++++++++++ 13 files changed, 392 insertions(+) create mode 100644 .gitignore create mode 100644 Containerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 compose.yml create mode 100644 composer.json create mode 100644 psalm.xml create mode 100644 src/Command/SearchIndexCommand.php create mode 100644 src/Entity/Interface/SearchableInterface.php create mode 100644 src/Entity/SearchIndex.php create mode 100644 src/EventSubscriber/SearchableSubscriber.php create mode 100644 src/PcmSearchBundle.php create mode 100644 src/Service/SearchService.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b7ef35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor +composer.lock diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..51f0222 --- /dev/null +++ b/Containerfile @@ -0,0 +1,7 @@ +FROM php:8.1-alpine + +WORKDIR /code +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer +COPY ./ /code + +RUN composer install \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..467a967 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +PHP = docker compose run php + + +composer_install: + @$(PHP) composer install + +static_analysis: + @$(PHP) vendor/bin/psalm \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..9bb369d --- /dev/null +++ b/compose.yml @@ -0,0 +1,7 @@ +services: + php: + build: + context: . + dockerfile: Containerfile + volumes: + - ./:/code \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ec33283 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "pcm/search-bundle", + "description": "Provides a search system for Symfony projects", + "type": "symfony-bundle", + "require": { + "php": ">=8.0", + "beberlei/doctrineextensions": "^1.3", + "doctrine/doctrine-bundle": "^2.7", + "doctrine/orm": "^2.12", + "symfony/framework-bundle": "*" + }, + "autoload": { + "psr-4": { + "Pcm\\SearchBundle\\": "src/" + } + }, + "authors": [ + { + "name": "Matt Feeny", + "email": "mf@pcmsystems.co.uk" + }, + { + "name": "Bradley Goode", + "email": "bg@pcmsystems.co.uk" + } + ], + "require-dev": { + "vimeo/psalm": "^4.24", + "psalm/plugin-symfony": "^3.1" + } +} diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..9ef218d --- /dev/null +++ b/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/src/Command/SearchIndexCommand.php b/src/Command/SearchIndexCommand.php new file mode 100644 index 0000000..74376ad --- /dev/null +++ b/src/Command/SearchIndexCommand.php @@ -0,0 +1,72 @@ +em->getRepository(SearchIndex::class); + $repo->clearIndex(); + + $searchables = $this->searchService->getSearchableClasses(); + + $io = new SymfonyStyle($input, $output); + $io->title('Indexing'); + + $progress_bar = new ProgressBar($output, count($searchables)); + $progress_bar->setBarCharacter('•'); + $progress_bar->setEmptyBarCharacter("•"); + $progress_bar->setProgressCharacter("➤"); + $progress_bar->start(); + + foreach ($searchables as $searchable) { + $entities = $this + ->em + ->getRepository($searchable) + ->findAll(); + + foreach ($entities as $entity) { + $search_result = $this->searchService->createSearchResult($entity); + + $this->em->persist($search_result); + + } + + $progress_bar->advance(); + } + + $this->em->flush(); + + $progress_bar->finish(); + + $io->writeln(''); + $io->writeln(''); + + $io->success('Index updated'); + + return Command::SUCCESS; + } +} diff --git a/src/Entity/Interface/SearchableInterface.php b/src/Entity/Interface/SearchableInterface.php new file mode 100644 index 0000000..66001f2 --- /dev/null +++ b/src/Entity/Interface/SearchableInterface.php @@ -0,0 +1,11 @@ +id; + } + + public function getEntityClass(): ?string + { + return $this->entityClass; + } + + public function setEntityClass(string $entityClass): self + { + $this->entityClass = $entityClass; + + return $this; + } + + public function getEntityId(): ?int + { + return $this->entityId; + } + + public function setEntityId(int $entityId): self + { + $this->entityId = $entityId; + + return $this; + } + + public function getData(): ?string + { + return $this->data; + } + + public function setData(string $data): self + { + $this->data = $data; + + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } +} diff --git a/src/EventSubscriber/SearchableSubscriber.php b/src/EventSubscriber/SearchableSubscriber.php new file mode 100644 index 0000000..0317843 --- /dev/null +++ b/src/EventSubscriber/SearchableSubscriber.php @@ -0,0 +1,52 @@ +getObject(); + + if ($entity instanceof SearchableInterface) { + $this->searchService->index($entity); + } + } + + public function postPersist(LifecycleEventArgs $args) + { + $entity = $args->getObject(); + + if ($entity instanceof SearchableInterface) { + $this->searchService->index($entity); + } + } + + public function preRemove(LifecycleEventArgs $args) + { + $entity = $args->getObject(); + + if ($entity instanceof SearchableInterface) { + $this->searchService->unIndex($entity); + } + } +} diff --git a/src/PcmSearchBundle.php b/src/PcmSearchBundle.php new file mode 100644 index 0000000..d534156 --- /dev/null +++ b/src/PcmSearchBundle.php @@ -0,0 +1,12 @@ +createSearchResult($entity); + + $this->em->persist($searchIndex); + $this->em->flush(); + } + + public function unIndex($entity) + { + $class = get_class($entity); + + $search_result = $this + ->em + ->getRepository(SearchIndex::class) + ->findOneBy(['entityClass' => $class, 'entityId' => $entity->getId()]); + + if ($search_result) { + $this->em->remove($search_result); + $this->em->flush(); + } + } + + public function createSearchResult(SearchableInterface $entity): SearchIndex + { + $values = []; + + foreach ($entity->getSearchValues() as $value) { + $values[] = $value; + } + + $data = implode(' ', $values); + + $class = get_class($entity); + + if ($entity instanceof Proxy) { + $class = ClassUtils::getRealClass($class); + } + + $searchResult = $this + ->em + ->getRepository(SearchIndex::class) + ->findOneBy( + [ + 'entityClass' => $class, + 'entityId' => $entity->getId() + ] + ); + + if (!$searchResult) { + $searchResult = new SearchIndex(); + $searchResult->setEntityClass($class); + $searchResult->setEntityId($entity->getId()); + } + + $searchResult->setTitle($entity->getSearchTitle()); + $searchResult->setData($data); + + return $searchResult; + } + + /** + * Finds all searchable Doctrine entities. + */ + public function getSearchableClasses(): array + { + $metadata = $this->em->getMetadataFactory()->getAllMetadata(); + $searchables = []; + + foreach ($metadata as $meta) { + if ($meta->reflClass->implementsInterface(SearchableInterface::class)) { + $searchables[] = $meta->name; + } + } + + return $searchables; + } +}