commit 06c5a90214e251f6f1e9ea71c0649a3226a5f6d4 Author: Matt Feeny Date: Wed Jul 20 09:23:11 2022 +0100 First pass of the search bundle 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; + } +}