First pass of the search bundle
This commit is contained in:
72
src/Command/SearchIndexCommand.php
Normal file
72
src/Command/SearchIndexCommand.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Pcm\SearchBundle\Command;
|
||||
|
||||
use Pcm\SearchBundle\Entity\SearchIndex;
|
||||
use Pcm\SearchBundle\Service\SearchService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'pcm:search:reindex',
|
||||
description: 'Builds search index',
|
||||
)]
|
||||
class SearchIndexCommand extends Command
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em, private SearchService $searchService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void {}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
/** @var SearchIndexRepository */
|
||||
$repo = $this->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('<fg=green>•</>');
|
||||
$progress_bar->setEmptyBarCharacter("<fg=red>•</>");
|
||||
$progress_bar->setProgressCharacter("<fg=green>➤</>");
|
||||
$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;
|
||||
}
|
||||
}
|
||||
11
src/Entity/Interface/SearchableInterface.php
Normal file
11
src/Entity/Interface/SearchableInterface.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pcm\SearchBundle\Entity\Interface;
|
||||
|
||||
interface SearchableInterface
|
||||
{
|
||||
public function getSearchTitle(): string;
|
||||
public function getSearchValues(): array;
|
||||
}
|
||||
81
src/Entity/SearchIndex.php
Normal file
81
src/Entity/SearchIndex.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Pcm\SearchBundle\Entity;
|
||||
|
||||
use Pcm\SearchBundle\Repository\SearchIndexRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: SearchIndexRepository::class)]
|
||||
#[ORM\Index(name: "idx_data_fulltext", columns: ["data"], flags: ["fulltext"])]
|
||||
class SearchIndex
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private $id;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255)]
|
||||
private $entityClass;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private $entityId;
|
||||
|
||||
#[ORM\Column(type: 'text')]
|
||||
private $data;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255)]
|
||||
private $title;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
||||
52
src/EventSubscriber/SearchableSubscriber.php
Normal file
52
src/EventSubscriber/SearchableSubscriber.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pcm\SearchBundle\EventSubscriber;
|
||||
|
||||
use Pcm\SearchBundle\Entity\Interface\SearchableInterface;
|
||||
use Pcm\SearchBundle\Service\SearchService;
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\Persistence\Event\LifecycleEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
class SearchableSubscriber implements EventSubscriber
|
||||
{
|
||||
public function __construct(private SearchService $searchService) {}
|
||||
|
||||
public function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
Events::postPersist,
|
||||
Events::postUpdate,
|
||||
Events::preRemove
|
||||
];
|
||||
}
|
||||
|
||||
public function postUpdate(LifecycleEventArgs $args)
|
||||
{
|
||||
$entity = $args->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/PcmSearchBundle.php
Normal file
12
src/PcmSearchBundle.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Pcm\SearchBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class SearchBundle extends Bundle
|
||||
{
|
||||
public function getPath(): string
|
||||
{
|
||||
return \dirname(__DIR__);
|
||||
}
|
||||
}
|
||||
94
src/Service/SearchService.php
Normal file
94
src/Service/SearchService.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pcm\SearchBundle\Service;
|
||||
|
||||
use Pcm\SearchBundle\Entity\SearchIndex;
|
||||
use Pcm\SearchBundle\Entity\Interface\SearchableInterface;
|
||||
use Doctrine\Common\Util\ClassUtils;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Proxy\Proxy;
|
||||
|
||||
class SearchService
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em) {}
|
||||
|
||||
public function index($entity)
|
||||
{
|
||||
$searchIndex = $this->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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user