Initial xentral_oss_20.3.c9ffacf

This commit is contained in:
Alex
2021-05-21 08:49:41 +02:00
parent 5406e4a551
commit 34e5ac43d9
18884 changed files with 2109867 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
<?php
namespace Xentral\Modules\SuperSearch;
use Xentral\Core\DependencyInjection\ContainerInterface;
use Xentral\Modules\SuperSearch\Factory\ProviderFactory;
use Xentral\Modules\SuperSearch\Scheduler\SuperSearchDiffIndexTask;
use Xentral\Modules\SuperSearch\Scheduler\SuperSearchFullIndexTask;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\AddressProvider;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\AppProvider;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\ArticleProvider;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\CreditNoteProvider;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\InvoiceProvider;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\OfferProvider;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\OrderProvider;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\DeliveryNoteProvider;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\TrackingNumberProvider;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\SearchIndexProviderInterface;
use Xentral\Modules\SuperSearch\SystemHealth\SuperSearchHealthChecker;
use Xentral\Modules\SuperSearch\Wrapper\CompanyConfigWrapper;
final class Bootstrap
{
/**
* @return array
*/
public static function registerServices()
{
return [
'SuperSearchService' => 'onInitSuperSearchService',
'SuperSearchIndexer' => 'onInitSuperSearchIndexer',
'SuperSearchEngine' => 'onInitSuperSearchEngine',
'SuperSearchProviderFactory' => 'onInitSuperSearchProviderFactory',
'SuperSearchHealthChecker' => 'onInitSuperSearchHealthChecker',
// Cronjob-Tasks
'SuperSearchFullIndexTask' => 'onInitSuperSearchFullIndexTask',
'SuperSearchDiffIndexTask' => 'onInitSuperSearchDiffIndexTask',
];
}
/**
* @param ContainerInterface $container
*
* @return SuperSearchService
*/
public static function onInitSuperSearchService(ContainerInterface $container)
{
return new SuperSearchService($container->get('Database'), $container->get('SuperSearchIndexer'));
}
/**
* @param ContainerInterface $container
*
* @return SuperSearchEngine
*/
public static function onInitSuperSearchEngine(ContainerInterface $container)
{
return new SuperSearchEngine($container->get('Database'));
}
/**
* @param ContainerInterface $container
*
* @return SuperSearchIndexer
*/
public static function onInitSuperSearchIndexer(ContainerInterface $container)
{
$provider = self::createSearchIndexProvider($container);
return new SuperSearchIndexer($container->get('Database'), $provider);
}
/**
* @param ContainerInterface $container
*
* @return ProviderFactory
*/
public static function onInitSuperSearchProviderFactory(ContainerInterface $container)
{
$factory = new ProviderFactory($container->get('Database'));
$factory->registerProviderFactory(
'addresses', static function (ContainerInterface $container) {
return new AddressProvider($container->get('Database'));
});
$factory->registerProviderFactory(
'articles', static function (ContainerInterface $container) {
return new ArticleProvider($container->get('Database'));
});
$factory->registerProviderFactory(
'creditnotes', static function (ContainerInterface $container) {
return new CreditNoteProvider($container->get('Database'));
});
$factory->registerProviderFactory(
'deliverynote', static function (ContainerInterface $container) {
return new DeliveryNoteProvider($container->get('Database'));
});
$factory->registerProviderFactory(
'invoices', static function (ContainerInterface $container) {
return new InvoiceProvider($container->get('Database'));
});
$factory->registerProviderFactory(
'offers', static function (ContainerInterface $container) {
return new OfferProvider($container->get('Database'));
});
$factory->registerProviderFactory(
'orders', static function (ContainerInterface $container) {
return new OrderProvider($container->get('Database'));
});
$factory->registerProviderFactory(
'trackingnumber', static function (ContainerInterface $container) {
return new TrackingNumberProvider($container->get('Database'));
});
$factory->registerProviderFactory(
'apps',
static function (ContainerInterface $container) {
/** @var \ApplicationCore $app */
$app = $container->get('LegacyApplication');
/** @var \Appstore $appstoreModule */
$appstoreModule = $app->erp->LoadModul('appstore');
return new AppProvider($appstoreModule);
}
);
return $factory;
}
/**
* @param ContainerInterface $container
*
* @return SuperSearchHealthChecker
*/
public static function onInitSuperSearchHealthChecker(ContainerInterface $container)
{
return new SuperSearchHealthChecker($container->get('Database'));
}
/**
* @param ContainerInterface $container
*
* @return SuperSearchFullIndexTask
*/
public static function onInitSuperSearchFullIndexTask(ContainerInterface $container)
{
/** @var SuperSearchService $service */
$service = $container->get('SuperSearchService');
/** @var SuperSearchIndexer $factory */
$indexer = $container->get('SuperSearchIndexer');
$config = self::onInitCompanyConfigWrapper($container);
return new SuperSearchFullIndexTask($service, $indexer, $config);
}
/**
* @param ContainerInterface $container
*
* @return SuperSearchDiffIndexTask
*/
public static function onInitSuperSearchDiffIndexTask(ContainerInterface $container)
{
/** @var SuperSearchService $service */
$service = $container->get('SuperSearchService');
/** @var SuperSearchIndexer $factory */
$indexer = $container->get('SuperSearchIndexer');
$config = self::onInitCompanyConfigWrapper($container);
return new SuperSearchDiffIndexTask($service, $indexer, $config);
}
/**
* @param ContainerInterface $container
*
* @return SearchIndexProviderInterface[]|array
*/
private static function createSearchIndexProvider(ContainerInterface $container)
{
/** @var ProviderFactory $factory */
$factory = $container->get('SuperSearchProviderFactory');
return $factory->createActiveProviders($container);
}
/**
* @param ContainerInterface $container
*
* @return CompanyConfigWrapper
*/
private static function onInitCompanyConfigWrapper(ContainerInterface $container)
{
/** @var \ApplicationCore $app */
$app = $container->get('LegacyApplication');
return new CompanyConfigWrapper($app->erp);
}
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Modules\SuperSearch\Exception;
use RuntimeException;
final class FormatterFailureException extends RuntimeException implements SuperSearchExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
namespace Xentral\Modules\SuperSearch\Exception;
final class InvalidArgumentException extends \InvalidArgumentException implements SuperSearchExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Modules\SuperSearch\Exception;
use LogicException;
final class InvalidReturnTypeException extends LogicException implements SuperSearchExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Modules\SuperSearch\Exception;
use RuntimeException;
final class InvalidReturnValueException extends RuntimeException implements SuperSearchExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Modules\SuperSearch\Exception;
use LogicException;
final class ProviderIncompatibleException extends LogicException implements SuperSearchExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Modules\SuperSearch\Exception;
use RuntimeException;
final class ProviderMissingException extends RuntimeException implements SuperSearchExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Modules\SuperSearch\Exception;
use RuntimeException;
final class SchedulerTaskAlreadyRunningException extends RuntimeException implements SuperSearchExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
namespace Xentral\Modules\SuperSearch\Exception;
interface SuperSearchExceptionInterface
{
}
@@ -0,0 +1,114 @@
<?php
namespace Xentral\Modules\SuperSearch\Factory;
use Xentral\Components\Database\Database;
use Xentral\Core\DependencyInjection\ContainerInterface;
use Xentral\Modules\SuperSearch\Exception\InvalidArgumentException;
use Xentral\Modules\SuperSearch\Exception\InvalidReturnTypeException;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\SearchIndexProviderInterface;
final class ProviderFactory
{
/** @var Database $db */
private $db;
/** @var array $callbacks */
private $callbacks = [];
/**
* @param Database $database
*/
public function __construct(Database $database)
{
$this->db = $database;
}
/**
* @param ContainerInterface $container
*
* @return array|SearchIndexProviderInterface[]
*/
public function createActiveProviders(ContainerInterface $container)
{
// Grundsätzlich sind alle registrierten Provider aktiv
$indexNames = array_keys($this->callbacks);
// Inaktiv markierte Provider aus Liste entfernen
$sql = 'SELECT sig.name FROM `supersearch_index_group` AS `sig` WHERE sig.active = 0';
$inactiveIndexes = $this->db->fetchCol($sql);
foreach ($inactiveIndexes as $inactiveIndex) {
$key = array_search($inactiveIndex, $indexNames, true);
if ($key !== false) {
unset($indexNames[$key]);
}
}
// Provider-Instanzen erzeugen
$providers = [];
foreach ($indexNames as $indexName) {
$providers[] = $this->createProvider($indexName, $container);
}
return $providers;
}
/**
* @param string $indexName
* @param callable $factoryMethod
*
* @throws InvalidArgumentException
*
* @return void
*/
public function registerProviderFactory($indexName, callable $factoryMethod)
{
if (!is_callable($factoryMethod, true)) {
throw new InvalidArgumentException(sprintf(
'Factory method for class "%s" is not callable.', $indexName
));
}
$this->callbacks[$indexName] = $factoryMethod;
}
/**
* @param string $className
*
* @return bool
*/
private function hasProviderFactory($className)
{
return isset($this->callbacks[$className]);
}
/**
* @param string $className
* @param ContainerInterface $container
*
* @throws InvalidArgumentException
* @throws InvalidReturnTypeException
*
* @return SearchIndexProviderInterface
*/
private function createProvider($className, ContainerInterface $container)
{
if (!$this->hasProviderFactory($className)) {
throw new InvalidArgumentException(sprintf(
'Provider class "%s" does not exists.', $className
));
}
$callback = $this->callbacks[$className];
$provider = $callback($container);
if (!$provider instanceof SearchIndexProviderInterface) {
throw new InvalidReturnTypeException(sprintf(
'Factory method for class "%s" returned invalid type. Provider must implement "%s".',
$className,
SearchIndexProviderInterface::class
));
}
return $provider;
}
}
@@ -0,0 +1,97 @@
<?php
namespace Xentral\Modules\SuperSearch\Scheduler;
use Xentral\Modules\SuperSearch\SuperSearchIndexer;
use Xentral\Modules\SuperSearch\SuperSearchService;
use Xentral\Modules\SuperSearch\Wrapper\CompanyConfigWrapper;
final class SuperSearchDiffIndexTask
{
/** @var SuperSearchService $service */
private $service;
/** @var SuperSearchIndexer $indexer */
private $indexer;
/** @var CompanyConfigWrapper $config */
private $config;
/**
* @param SuperSearchService $service
* @param SuperSearchIndexer $indexer
* @param CompanyConfigWrapper $config
*/
public function __construct(SuperSearchService $service, SuperSearchIndexer $indexer, CompanyConfigWrapper $config)
{
$this->service = $service;
$this->indexer = $indexer;
$this->config = $config;
}
/**
* @return void
*/
public function execute()
{
$fullIndexCronjobActive = (int)$this->config->get('supersearch_full_index_task_mutex');
// Diff-Index nur ausführen wenn Full-Index-Cronjob gerade nicht läuft
if ($fullIndexCronjobActive === 0) {
$this->updateIndexes();
}
// Full-Index läuft gerade > Zähler erhöhen
if ($fullIndexCronjobActive > 0) {
$this->config->set('supersearch_full_index_task_mutex', (string)($fullIndexCronjobActive + 1));
}
// Full-Index läuft schon sehr lange (eher ein Fehler) > Zähler zurücksetzen
if ($fullIndexCronjobActive > 3) {
$this->config->set('supersearch_full_index_task_mutex', '0');
}
}
/**
* @return void
*/
private function updateIndexes()
{
$meta = $this->indexer->getProviderMetaData();
foreach ($meta as $row) {
$indexName = $row['name'];
$indexTitle = $row['title'];
$moduleName = $row['module'];
// Sicherstellen dass die Indexe für die registrierten Provider vorhanden sind
if (!$this->service->existsIndex($indexName)) {
$this->service->createIndex($indexName, $indexTitle, $moduleName);
}
/*
* Such-Index befüllen
*/
// Diff-Index wurde schon einmal ausgeführt > Diff-Index wieder ausführen
$lastDiffIndexTime = $this->indexer->getLastDiffIndexTime($indexName);
if ($lastDiffIndexTime !== null) {
$this->indexer->updateIndexSince($indexName, $lastDiffIndexTime);
continue;
}
// Diff-Index und Full-Index wurden noch nie ausgeführt => FullIndex ausführen
$lastFullIndexTime = $this->indexer->getLastFullIndexTime($indexName);
if ($lastDiffIndexTime === null && $lastFullIndexTime === null) {
$this->indexer->updateIndexFull($indexName);
}
}
}
/**
* @return void
*/
public function cleanup()
{
// Nothing to do
}
}
@@ -0,0 +1,83 @@
<?php
namespace Xentral\Modules\SuperSearch\Scheduler;
use Xentral\Modules\SuperSearch\Exception\SchedulerTaskAlreadyRunningException;
use Xentral\Modules\SuperSearch\Exception\SuperSearchExceptionInterface;
use Xentral\Modules\SuperSearch\SuperSearchIndexer;
use Xentral\Modules\SuperSearch\SuperSearchService;
use Xentral\Modules\SuperSearch\Wrapper\CompanyConfigWrapper;
final class SuperSearchFullIndexTask
{
/** @var SuperSearchService $service */
private $service;
/** @var SuperSearchIndexer $indexer */
private $indexer;
/** @var CompanyConfigWrapper $config */
private $config;
/**
* @param SuperSearchService $service
* @param SuperSearchIndexer $indexer
* @param CompanyConfigWrapper $config
*/
public function __construct(SuperSearchService $service, SuperSearchIndexer $indexer, CompanyConfigWrapper $config)
{
$this->service = $service;
$this->indexer = $indexer;
$this->config = $config;
}
/**
* @throws SuperSearchExceptionInterface
*
* @return void
*/
public function execute()
{
// Prüfen ob Full-Index-Cronjob bereits läuft > Mehrfachausführung verhindern
$fullIndexActive = (int)$this->config->get('supersearch_full_index_task_mutex');
if ($fullIndexActive > 0) {
throw new SchedulerTaskAlreadyRunningException(
'SuperSearch full index task is already running. Task can only run once at a time.'
);
}
// Full-Index-Cronjob als Aktiv markieren >
// Diff-Index-Cronjob prüft den Wert und überspringt dann wenn Full-Index-Cronjob läuft.
$this->config->set('supersearch_full_index_task_mutex', '1');
$this->updateIndexes();
}
/**
* @return void
*/
public function cleanup()
{
$this->config->set('supersearch_full_index_task_mutex', '0');
}
/**
* @return void
*/
private function updateIndexes()
{
$meta = $this->indexer->getProviderMetaData();
foreach ($meta as $row) {
$indexName = $row['name'];
$indexTitle = $row['title'];
$moduleName = $row['module'];
// Sicherstellen dass die Indexe für die registrierten Provider vorhanden sind
if (!$this->service->existsIndex($indexName)) {
$this->service->createIndex($indexName, $indexTitle, $moduleName);
}
// Such-Index befüllen
$this->indexer->updateIndexFull($indexName);
}
}
}
@@ -0,0 +1,81 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchEngine;
use Xentral\Components\Util\StringUtil;
final class SearchTermParser
{
/**
* @internal Operators in Boolean search mode: +, -, > <, ( ), ~, *, ", @distance
*
* @param string $searchTerm
*
* @return string
*/
public function parse($searchTerm)
{
// Remove unused (not supported by us) search operators: *, > <, ( ), @distance
$searchTerm = preg_replace('/[><()*@]+/', '', $searchTerm);
$searchWords = preg_split('/([\s]+)/um', $searchTerm, -1, PREG_SPLIT_NO_EMPTY);
if ($searchWords === false) {
return '';
}
foreach ($searchWords as &$searchWord) {
if (strlen($searchWord) < 3) {
$searchWord = '';
continue;
}
$hasLeadingTilde = StringUtil::startsWith($searchWord, '~');
$searchWord = ltrim($searchWord, '~');
$hasLeadingMinus = StringUtil::startsWith($searchWord, '-');
$searchWord = trim($searchWord, '-'); // Trim both sides; InnoDB only supports leading minus signs
$hasLeadingPlus = StringUtil::startsWith($searchWord, '+');
$searchWord = trim($searchWord, '+'); // Trim both sides; InnoDB only supports leading plus signs
$hasLeadingQuote = StringUtil::startsWith($searchWord, '"');
$hasTrailingQuote = StringUtil::endsWith($searchWord, '"');
$searchWord = trim($searchWord, '"');
// Operatoren innerhalb eines Suchworts entfernen
$searchWord = preg_replace('/[+\-~\"]+/', '', $searchWord);
$searchWord = trim($searchWord);
if (strlen($searchWord) < 3) {
$searchWord = ''; // Zu kurze Suchwörter ignorieren
continue;
}
// Beispiel: `"foobar"`
// Kombination mit Operatoren möglich: `+"foobar"` oder `-"foobar"` oder `~"foobar"`
if ($hasLeadingQuote && $hasTrailingQuote) {
$searchWord = '"' . $searchWord . '"';
}
// Operatoren vorne anfügen
if ($hasLeadingTilde) {
$searchWord = '~' . $searchWord;
}
if ($hasLeadingMinus) {
$searchWord = '-' . $searchWord;
}
if ($hasLeadingPlus) {
$searchWord = '+' . $searchWord;
}
// Default: Wildcard hinten, nur wenn keine Anführungszeichen
if (!$hasLeadingQuote && !$hasTrailingQuote) {
$searchWord .= '*';
}
}
unset($searchWord);
return trim(implode(' ', $searchWords));
}
}
@@ -0,0 +1,128 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Collection;
use ArrayObject;
use Closure;
use Exception;
use Iterator;
use IteratorAggregate;
use Xentral\Modules\SuperSearch\Exception\InvalidArgumentException;
use Xentral\Modules\SuperSearch\Exception\InvalidReturnTypeException;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
final class ItemFormatterCollection implements Iterator
{
/** @var Iterator $data */
private $data;
/** @var callable $callback */
private $callback;
/**
* @param array|Iterator $data
* @param callable|Closure $callback
*
* @throws InvalidArgumentException
*/
public function __construct($data, $callback)
{
if (!is_callable($callback, false)) {
throw new InvalidArgumentException('Callback is not callable');
}
$this->callback = $callback;
$type = gettype($data);
if ($type === 'object') {
$type = get_class($data);
if ($data instanceof Iterator) {
$type = 'Iterator';
}
if ($data instanceof IteratorAggregate) {
$type = 'IteratorAggregate';
}
}
switch ($type) {
case 'array':
$this->data = (new ArrayObject($data))->getIterator();
break;
case 'Iterator':
$this->data = $data;
break;
case 'IteratorAggregate':
try {
$this->data = $data->getIterator();
} catch (Exception $exception) {
throw new InvalidArgumentException($exception->getMessage(), $exception->getCode(), $exception);
}
break;
default:
throw new InvalidArgumentException(sprintf('Unsupported type "%s".', $type));
break;
}
}
/**
* Return the current element
*
* @throws InvalidReturnTypeException
*
* @return IndexItem
*/
public function current()
{
$result = call_user_func($this->callback, $this->data->current(), $this->data->key());
if (!$result instanceof IndexItem) {
throw new InvalidReturnTypeException(sprintf(
'Formatter return type is invalid . Callable must return an object with type "%s".',
IndexItem::class
));
}
return $result;
}
/**
* Move forward to next element
*
* @return void
*/
public function next()
{
$this->data->next();
}
/**
* Return the key of the current element
*
* @return mixed scalar on success, or null on failure.
*/
public function key()
{
return $this->data->key();
}
/**
* Checks if current position is valid
*
* @return boolean Returns true on success or false on failure.
*/
public function valid()
{
return $this->data->valid();
}
/**
* Rewind the Iterator to the first element
*
* @return void
*/
public function rewind()
{
$this->data->rewind();
}
}
@@ -0,0 +1,172 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Data;
use Xentral\Modules\SuperSearch\Exception\InvalidArgumentException;
final class IndexData
{
/** @var int $projectId */
private $projectId;
/** @var string $title */
private $title;
/** @var string|null $subTitle */
private $subTitle;
/** @var array|string[] $additionalInfos */
private $additionalInfos;
/** @var string $link */
private $link;
/** @var array $words */
private $words;
/**
* @param string $title
* @param string $link
* @param int $projectId
* @param array $words
*
* @throws InvalidArgumentException
*/
public function __construct($title, $link, $projectId = 0, array $words = [])
{
if (empty($title)) {
$title = 'empty';
}
if (empty($link)) {
throw new InvalidArgumentException('Invalid argument value. $link parameter can not be empty.');
}
if (!is_int($projectId)) {
throw new InvalidArgumentException('Invalid argument type. Parameter $projectId must be type integer.');
}
$this->projectId = (int)$projectId;
$this->title = (string)$title;
$this->link = (string)$link;
$this->addSearchWords($words);
}
/**
* @param array $state
*
* @return self
*/
public static function fromDbState(array $state)
{
return new self(
(string)$state['title'],
(string)$state['link'],
(int)$state['project_id'],
(array)$state['search_words']
);
}
/**
* @param string $subTitle
*
* @return void
*/
public function setSubTitle($subTitle)
{
$subTitle = trim($subTitle);
if ($subTitle === '') {
return;
}
$this->subTitle = (string)$subTitle;
}
/**
* @param string $additionalInfo
*
* @return void
*/
public function addAdditionalInfo($additionalInfo)
{
$additionalInfo = trim($additionalInfo);
if ($additionalInfo === '') {
return;
}
$this->additionalInfos[] = (string)$additionalInfo;
}
/**
* @param array|string[] $words
*
* @return void
*/
public function addSearchWords(array $words)
{
foreach ($words as $word) {
$this->addSearchWord($word);
}
}
/**
* @param string $word
*
* @return void
*/
public function addSearchWord($word)
{
$word = trim($word);
if ($word === '') {
return;
}
$this->words[] = $word;
}
/**
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* @return string|null
*/
public function getSubTitle()
{
return $this->subTitle;
}
/**
* @return array|string[]
*/
public function getAdditionalInfos()
{
return $this->additionalInfos;
}
/**
* @return string
*/
public function getLink()
{
return $this->link;
}
/**
* @return int
*/
public function getProjectId()
{
return $this->projectId;
}
/**
* @return array
*/
public function getWords()
{
return $this->words;
}
}
@@ -0,0 +1,64 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Data;
use Xentral\Modules\SuperSearch\Exception\InvalidArgumentException;
final class IndexIdentifier
{
/** @var string $name */
private $name;
/** @var int $id */
private $id;
/**
* @param string $name
* @param int|string $id
*
* @throws InvalidArgumentException
*/
public function __construct($name, $id)
{
if (empty($name)) {
throw new InvalidArgumentException('Invalid argument value. $name parameter can not be empty.');
}
if (empty($id)) {
throw new InvalidArgumentException('Invalid argument value. $id parameter can not be empty.');
}
if (strlen($name) > 16) {
throw new InvalidArgumentException(sprintf(
'Invalid argument value "%s". Max length for $id is 16 characters.', $name
));
}
if (strlen($id) > 38) {
throw new InvalidArgumentException(sprintf(
'Invalid argument value "%s". Max length for $id is 38 characters.', $id
));
}
if (preg_match('/[^a-z]+/', $name) === 1) {
throw new InvalidArgumentException(sprintf(
'Invalid argument value "%s". Valid characters for $name parameter: a-z', $name
));
}
$this->name = (string)$name;
$this->id = is_numeric($id) ? (int)$id : (string)$id;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return int|string
*/
public function getId()
{
return $this->id;
}
}
@@ -0,0 +1,27 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Data;
final class IndexItem
{
/** @var IndexIdentifier $identifier */
public $identifier;
/** @var IndexData $data */
public $data;
/** @var string|null $module */
public $module;
/**
* @param IndexIdentifier $identifier
* @param IndexData $data
* @param string|null $moduleName
*/
public function __construct(IndexIdentifier $identifier, IndexData $data, $moduleName = null)
{
$this->identifier = $identifier;
$this->data = $data;
$this->module = $moduleName;
}
}
@@ -0,0 +1,188 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use Closure;
use DateTimeInterface;
use Exception;
use Xentral\Components\Database\Database;
use Xentral\Components\Database\SqlQuery\SelectQuery;
use Xentral\Modules\SuperSearch\Exception\FormatterFailureException;
use Xentral\Modules\SuperSearch\Exception\InvalidReturnTypeException;
use Xentral\Modules\SuperSearch\Exception\SuperSearchExceptionInterface;
use Xentral\Modules\SuperSearch\SearchIndex\Collection\ItemFormatterCollection;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
abstract class AbstractBulkIndexDatabaseProvider implements
SearchIndexProviderInterface,
BulkIndexProviderInterface,
ItemIndexProviderInterface,
DiffIndexProviderInterface
{
/** @var Database $db */
protected $db;
/**
* @param Database $database
*/
public function __construct(Database $database)
{
$this->db = $database;
}
/**
* @param SelectQuery $query
*
* @return void
*/
abstract protected function configureBaseQuery(SelectQuery $query);
/**
* @param SelectQuery $baseQuery
* @param int|string $indexId
*
* @return void
*/
abstract protected function configureItemQuery(SelectQuery $baseQuery, $indexId);
/**
* @param SelectQuery $baseQuery
* @param DateTimeInterface $since
*
* @return void
*/
abstract protected function configureSinceQuery(SelectQuery $baseQuery, DateTimeInterface $since);
/**
* @param SelectQuery $baseQuery
*
* @return void
*/
abstract protected function configureCountQuery(SelectQuery $baseQuery);
/**
* @return Closure
*/
abstract protected function getRowFormatter();
/**
* @return int Anzahl der IndexItems die in einem Durchlauf geschrieben werden
*/
public function getBulkSize()
{
return 20000;
}
/**
* @param IndexIdentifier $identifier
*
* @throws InvalidReturnTypeException
*
* @return IndexItem|null
*/
public function getItem(IndexIdentifier $identifier)
{
$select = $this->db->select();
$this->configureBaseQuery($select);
$this->configureItemQuery($select, $identifier->getId());
$row = $this->db->fetchRow(
$select->getStatement(),
$select->getBindValues()
);
if (empty($row)) {
return null;
}
try {
// Formatter aufrufen
$formatter = $this->getRowFormatter();
$item = $formatter($row);
} catch (SuperSearchExceptionInterface $exception) {
throw new FormatterFailureException(
sprintf('Formatter failed. Row data: %s', var_export($row, true)),
$exception->getCode(),
$exception
);
}
// Prüfen ob Formatter den richtigen Typ zurückliefert
if (!$item instanceof IndexItem) {
$itemType = gettype($item);
if ($itemType === 'object') {
$itemType = get_class($item);
}
throw new InvalidReturnTypeException(sprintf(
'"%s::getRowFormatter()" returned invalid type. Required type "%s". Returned type "%s".',
get_class($this),
IndexItem::class,
$itemType
));
}
return $item;
}
/**
* @inheritDoc
* @throws Exception
*/
public function getItemsSince(DateTimeInterface $since)
{
$select = $this->db->select();
$this->configureBaseQuery($select);
$this->configureSinceQuery($select, $since);
$callback = $this->getRowFormatter();
$data = $this->db->fetchAll(
$select->getStatement(),
$select->getBindValues()
);
return new ItemFormatterCollection($data, $callback);
}
/**
* @inheritDoc
*/
public function getTotalCount()
{
$select = $this->db->select();
$this->configureBaseQuery($select);
$select->resetCols();
$this->configureCountQuery($select);
return (int)$this->db->fetchValue(
$select->getStatement(),
$select->getBindValues()
);
}
/**
* @param int $offset
* @param int $count
*
* @throws InvalidReturnTypeException
* @throws Exception
*
* @return ItemFormatterCollection
*/
public function getBulkItems($offset, $count)
{
$select = $this->db->select();
$this->configureBaseQuery($select);
$select->offset($offset);
$select->limit($count);
$callback = $this->getRowFormatter();
$data = $this->db->fetchAll(
$select->getStatement(),
$select->getBindValues()
);
return new ItemFormatterCollection($data, $callback);
}
}
@@ -0,0 +1,134 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use DateTimeInterface;
use Xentral\Components\Database\SqlQuery\SelectQuery;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexData;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
final class AddressProvider extends AbstractBulkIndexDatabaseProvider
{
/**
* @inheritDoc
*/
public function getModuleName()
{
return 'adresse';
}
/**
* @inheritDoc
*/
public function getIndexName()
{
return 'addresses';
}
/**
* @inheritDoc
*/
public function getIndexTitle()
{
return 'Adressen';
}
/**
* @inheritDoc
*/
protected function configureBaseQuery(SelectQuery $query)
{
$query
->cols([
'a.id',
'a.projekt',
'a.name',
'a.abteilung',
'a.unterabteilung',
'a.ansprechpartner',
'a.strasse',
'a.ort',
'a.plz',
'a.adresszusatz',
'a.telefon',
'a.telefax',
'a.mobil',
'a.email',
'a.ustid',
'a.kundennummer',
'a.lieferantennummer',
'a.mitarbeiternummer',
])
->from('adresse AS a')
->where('a.geloescht = ?', 0);
}
/**
* @inheritDoc
*/
protected function configureItemQuery(SelectQuery $baseQuery, $indexId)
{
$baseQuery->where('a.id = ?', (int)$indexId);
}
/**
* @inheritDoc
*/
protected function configureCountQuery(SelectQuery $baseQuery)
{
$baseQuery->cols(['COUNT(a.id)' => 'total_count']);
}
/**
* @inheritDoc
*/
protected function configureSinceQuery(SelectQuery $baseQuery, DateTimeInterface $since)
{
$baseQuery->where('a.logdatei > ?', $since->format('Y-m-d H:m:i'));
}
/**
* @inheritDoc
*/
protected function getRowFormatter()
{
return static function (array $row) {
$projectId = (int)$row['projekt'];
$title = $row['name'];
$link = sprintf('index.php?module=adresse&action=edit&id=%d', $row['id']);
$data = new IndexData($title, $link, $projectId);
if (!empty($row['kundennummer'])) {
$data->addAdditionalInfo(sprintf('Kunde %s', $row['kundennummer']));
}
if (!empty($row['lieferantennummer'])) {
$data->addAdditionalInfo(sprintf('Lieferant %s', $row['lieferantennummer']));
}
if (!empty($row['mitarbeiternummer'])) {
$data->addAdditionalInfo(sprintf('Mitarbeiter %s', $row['mitarbeiternummer']));
}
$data->addSearchWord($row['name']);
$data->addSearchWord($row['abteilung']);
$data->addSearchWord($row['unterabteilung']);
$data->addSearchWord($row['ansprechpartner']);
$data->addSearchWord($row['strasse']);
$data->addSearchWord($row['ort']);
$data->addSearchWord($row['plz']);
$data->addSearchWord($row['adresszusatz']);
$data->addSearchWord($row['telefon']);
$data->addSearchWord($row['telefax']);
$data->addSearchWord($row['mobil']);
$data->addSearchWord($row['email']);
$data->addSearchWord($row['ustid']);
$data->addSearchWord($row['kundennummer']);
$data->addSearchWord($row['lieferantennummer']);
$data->addSearchWord($row['mitarbeiternummer']);
$identifier = new IndexIdentifier('addresses', (int)$row['id']);
return new IndexItem($identifier, $data);
};
}
}
@@ -0,0 +1,104 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use Appstore;
use Exception;
use Xentral\Modules\SuperSearch\SearchIndex\Collection\ItemFormatterCollection;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexData;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
final class AppProvider implements FullIndexProviderInterface, ItemIndexProviderInterface
{
/** @var Appstore $appstore */
private $appstore;
/**
* @param Appstore $appstore
*/
public function __construct(Appstore $appstore)
{
$this->appstore = $appstore;
}
/**
* @inheritDoc
*/
public function getModuleName()
{
return 'appstore';
}
/**
* @inheritDoc
*/
public function getIndexName()
{
return 'apps';
}
/**
* @inheritDoc
*/
public function getIndexTitle()
{
return 'Apps';
}
/**
* @inheritDoc
*/
public function getItem(IndexIdentifier $identifier)
{
$moduleKey = $identifier->getId();
$modules = $this->appstore->BuildModuleList();
if (!isset($modules[$moduleKey])) {
return null;
}
$formatter = $this->getRowFormatter();
return $formatter($modules[$moduleKey]);
}
/**
* @inheritDoc
*
* @throws Exception
*/
public function getAllItems()
{
$callback = $this->getRowFormatter();
$modules = $this->appstore->BuildModuleList();
unset($modules['appstore_extern']);
// Module ohne Link entfernen
foreach ($modules as $moduleName => $moduleData) {
if (empty($moduleData['module_link'])) {
unset($modules[$moduleName]);
}
}
return new ItemFormatterCollection($modules, $callback);
}
/**
* @inheritDoc
*/
protected function getRowFormatter()
{
return static function (array $module) {
$projectId = 0;
$data = new IndexData($module['title'], $module['module_link'], $projectId);
$data->addSearchWord($module['title']);
$data->addSearchWord(html_entity_decode($module['title']));
$data->addSearchWord($module['description']);
$data->addSearchWord($module['category']);
$identifier = new IndexIdentifier('apps', $module['key']);
return new IndexItem($identifier, $data);
};
}
}
@@ -0,0 +1,103 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use DateTimeInterface;
use Xentral\Components\Database\SqlQuery\SelectQuery;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexData;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
final class ArticleProvider extends AbstractBulkIndexDatabaseProvider
{
/**
* @inheritDoc
*/
public function getModuleName()
{
return 'artikel';
}
/**
* @inheritDoc
*/
public function getIndexName()
{
return 'articles';
}
/**
* @inheritDoc
*/
public function getIndexTitle()
{
return 'Artikel';
}
/**
* @inheritDoc
*/
protected function configureBaseQuery(SelectQuery $query)
{
$query
->cols([
'a.id',
'a.projekt',
'a.nummer',
'a.name_de',
'a.kurztext_de',
'a.herstellernummer',
'a.ean',
])
->from('artikel AS a')
->where('a.geloescht = ?', 0);
}
/**
* @inheritDoc
*/
protected function configureItemQuery(SelectQuery $baseQuery, $indexId)
{
$baseQuery->where('a.id = ?', (int)$indexId);
}
/**
* @inheritDoc
*/
protected function configureCountQuery(SelectQuery $baseQuery)
{
$baseQuery->cols(['COUNT(a.id)' => 'total_count']);
}
/**
* @inheritDoc
*/
protected function configureSinceQuery(SelectQuery $baseQuery, DateTimeInterface $since)
{
$baseQuery->where('a.logdatei > ?', $since->format('Y-m-d H:m:i'));
}
/**
* @inheritDoc
*/
protected function getRowFormatter()
{
return static function (array $row) {
$projectId = (int)$row['projekt'];
$link = sprintf('index.php?module=artikel&action=edit&id=%d', $row['id']);
$data = new IndexData($row['nummer'], $link, $projectId);
$data->setSubTitle($row['name_de']);
$data->addSearchWord($row['nummer']);
$data->addSearchWord($row['name_de']);
$data->addSearchWord($row['kurztext_de']);
$data->addSearchWord($row['herstellernummer']);
$data->addSearchWord($row['ean']);
$identifier = new IndexIdentifier('articles', (int)$row['id']);
return new IndexItem($identifier, $data);
};
}
}
@@ -0,0 +1,32 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use Xentral\Modules\SuperSearch\SearchIndex\Collection\ItemFormatterCollection;
interface BulkIndexProviderInterface extends SearchIndexProviderInterface
{
/**
* Returns a range of the index items
*
* @param int $start
* @param int $limit
*
* @return ItemFormatterCollection
*/
public function getBulkItems($start, $limit);
/**
* Returns the number of items to process in one transaction
*
* @return int
*/
public function getBulkSize();
/**
* Returns the total count for all items
*
* @return int
*/
public function getTotalCount();
}
@@ -0,0 +1,118 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use DateTimeInterface;
use Xentral\Components\Database\SqlQuery\SelectQuery;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexData;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
final class CreditNoteProvider extends AbstractBulkIndexDatabaseProvider
{
/**
* @inheritDoc
*/
public function getModuleName()
{
return 'gutschrift';
}
/**
* @inheritDoc
*/
public function getIndexName()
{
return 'creditnotes';
}
/**
* @inheritDoc
*/
public function getIndexTitle()
{
return 'Gutschriften';
}
/**
* @inheritDoc
*/
protected function configureBaseQuery(SelectQuery $query)
{
$query
->cols([
'gs.id',
'gs.projekt',
'gs.status',
'gs.datum',
'gs.belegnr',
'gs.rechnung',
'gs.name',
'gs.kundennummer',
'gs.internebemerkung',
'gs.ihrebestellnummer',
'gs.soll',
'gs.zahlungsstatus',
'gs.waehrung',
])
->from('gutschrift AS gs')
->where('gs.belegnr != ?', '');
}
/**
* @inheritDoc
*/
protected function configureItemQuery(SelectQuery $baseQuery, $indexId)
{
$baseQuery->where('gs.id = ?', (int)$indexId);
}
/**
* @inheritDoc
*/
protected function configureCountQuery(SelectQuery $baseQuery)
{
$baseQuery->cols(['COUNT(gs.id)' => 'total_count']);
}
/**
* @inheritDoc
*/
protected function configureSinceQuery(SelectQuery $baseQuery, DateTimeInterface $since)
{
$baseQuery->where('gs.logdatei > ?', $since->format('Y-m-d H:m:i'));
}
/**
* @inheritDoc
*/
protected function getRowFormatter()
{
return static function (array $row) {
$projectId = (int)$row['projekt'];
$rechnungsDatum = date('d.m.Y', strtotime($row['datum']));
$title = $row['belegnr'];
$link = sprintf('index.php?module=gutschrift&action=edit&id=%d', $row['id']);
$data = new IndexData($title, $link, $projectId);
$data->setSubTitle($row['name']);
$data->addAdditionalInfo($rechnungsDatum);
$data->addAdditionalInfo(ucfirst($row['status']));
$data->addAdditionalInfo(sprintf('%s %s', number_format($row['soll'], 2, ',', '.'), $row['waehrung']));
$data->addSearchWord('gutschrift');
$data->addSearchWord($row['belegnr']);
$data->addSearchWord($row['rechnung']);
$data->addSearchWord($row['status']);
$data->addSearchWord($row['name']);
$data->addSearchWord($row['kundennummer']);
$data->addSearchWord($row['internebemerkung']);
$data->addSearchWord($row['ihrebestellnummer']);
$identifier = new IndexIdentifier('creditnotes', (int)$row['id']);
return new IndexItem($identifier, $data);
};
}
}
@@ -0,0 +1,111 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use DateTimeInterface;
use Xentral\Components\Database\SqlQuery\SelectQuery;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexData;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
final class DeliveryNoteProvider extends AbstractBulkIndexDatabaseProvider
{
/**
* @inheritDoc
*/
public function getModuleName()
{
return 'lieferschein';
}
/**
* @inheritDoc
*/
public function getIndexName()
{
return 'deliverynote';
}
/**
* @inheritDoc
*/
public function getIndexTitle()
{
return 'Lieferscheine';
}
/**
* @inheritDoc
*/
protected function configureBaseQuery(SelectQuery $query)
{
$query
->cols([
'l.id',
'l.projekt',
'l.datum',
'l.belegnr',
'l.status',
'l.name',
'l.kundennummer',
'l.internebezeichnung'
])
->from('lieferschein AS l')
->where('l.belegnr != ?', '');
}
/**
* @inheritDoc
*/
protected function configureItemQuery(SelectQuery $baseQuery, $indexId)
{
$baseQuery->where('l.id = ?', (int)$indexId);
}
/**
* @inheritDoc
*/
protected function configureCountQuery(SelectQuery $baseQuery)
{
$baseQuery->cols(['COUNT(l.id)' => 'total_count']);
}
/**
* @inheritDoc
*/
protected function configureSinceQuery(SelectQuery $baseQuery, DateTimeInterface $since)
{
$baseQuery->where('l.logdatei > ?', $since->format('Y-m-d H:m:i'));
}
/**
* @inheritDoc
*/
protected function getRowFormatter()
{
return static function (array $row) {
$projectId = (int)$row['projekt'];
$lieferscheinDatum = date('d.m.Y', strtotime($row['datum']));
$title = $row['belegnr'];
$link = sprintf('index.php?module=lieferschein&action=edit&id=%d', $row['id']);
$data = new IndexData($title, $link, $projectId);
$data->addSearchWord('lieferschein');
$data->addSearchWord($row['belegnr']);
$data->addSearchWord($row['name']);
$data->addSearchWord($row['kundennummer']);
$data->addSearchWord($row['internebezeichnung']);
$data->setSubTitle($row['name']);
$data->addAdditionalInfo($lieferscheinDatum);
$data->addAdditionalInfo($row['status']);
$identifier = new IndexIdentifier('deliverynote', (int)$row['id']);
return new IndexItem($identifier, $data);
};
}
}
@@ -0,0 +1,18 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use DateTimeInterface;
use Xentral\Modules\SuperSearch\SearchIndex\Collection\ItemFormatterCollection;
interface DiffIndexProviderInterface extends SearchIndexProviderInterface
{
/**
* Returns index items that were changed or created since the passed date time
*
* @param DateTimeInterface $since
*
* @return ItemFormatterCollection
*/
public function getItemsSince(DateTimeInterface $since);
}
@@ -0,0 +1,17 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use Xentral\Modules\SuperSearch\SearchIndex\Collection\ItemFormatterCollection;
interface FullIndexProviderInterface extends SearchIndexProviderInterface
{
/**
* Returns all items; No sectioning, just all items
*
* Use BulkIndexProviderInterface for large datasets instead
*
* @return ItemFormatterCollection
*/
public function getAllItems();
}
@@ -0,0 +1,116 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use DateTimeInterface;
use Xentral\Components\Database\SqlQuery\SelectQuery;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexData;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
final class InvoiceProvider extends AbstractBulkIndexDatabaseProvider
{
/**
* @inheritDoc
*/
public function getModuleName()
{
return 'rechnung';
}
/**
* @inheritDoc
*/
public function getIndexName()
{
return 'invoices';
}
/**
* @inheritDoc
*/
public function getIndexTitle()
{
return 'Rechnungen';
}
/**
* @inheritDoc
*/
protected function configureBaseQuery(SelectQuery $query)
{
$query
->cols([
'r.id',
'r.projekt',
'r.datum',
'r.belegnr',
'r.auftrag',
'r.name',
'r.kundennummer',
'r.internebemerkung',
'r.ihrebestellnummer',
'r.soll',
'r.zahlungsstatus',
'r.waehrung',
])
->from('rechnung AS r')
->where('r.belegnr != ?', '');
}
/**
* @inheritDoc
*/
protected function configureItemQuery(SelectQuery $baseQuery, $indexId)
{
$baseQuery->where('r.id = ?', (int)$indexId);
}
/**
* @inheritDoc
*/
protected function configureCountQuery(SelectQuery $baseQuery)
{
$baseQuery->cols(['COUNT(r.id)' => 'total_count']);
}
/**
* @inheritDoc
*/
protected function configureSinceQuery(SelectQuery $baseQuery, DateTimeInterface $since)
{
$baseQuery->where('r.logdatei > ?', $since->format('Y-m-d H:m:i'));
}
/**
* @inheritDoc
*/
protected function getRowFormatter()
{
return static function (array $row) {
$projectId = (int)$row['projekt'];
$rechnungsDatum = date('d.m.Y', strtotime($row['datum']));
$title = $row['belegnr'];
$link = sprintf('index.php?module=rechnung&action=edit&id=%d', $row['id']);
$data = new IndexData($title, $link, $projectId);
$data->setSubTitle($row['name']);
$data->addAdditionalInfo($rechnungsDatum);
$data->addAdditionalInfo(ucfirst($row['zahlungsstatus']));
$data->addAdditionalInfo(sprintf('%s %s', number_format($row['soll'], 2, ',', '.'), $row['waehrung']));
$data->addSearchWord('rechnung');
$data->addSearchWord($row['belegnr']);
$data->addSearchWord($row['auftrag']);
$data->addSearchWord($row['name']);
$data->addSearchWord($row['kundennummer']);
$data->addSearchWord($row['internebemerkung']);
$data->addSearchWord($row['ihrebestellnummer']);
$identifier = new IndexIdentifier('invoices', (int)$row['id']);
return new IndexItem($identifier, $data);
};
}
}
@@ -0,0 +1,18 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
interface ItemIndexProviderInterface extends SearchIndexProviderInterface
{
/**
* Returns a single item for writing into the search index
*
* @param IndexIdentifier $identifier
*
* @return IndexItem|null
*/
public function getItem(IndexIdentifier $identifier);
}
@@ -0,0 +1,115 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use DateTimeInterface;
use Xentral\Components\Database\SqlQuery\SelectQuery;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexData;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
final class OfferProvider extends AbstractBulkIndexDatabaseProvider
{
/**
* @inheritDoc
*/
public function getModuleName()
{
return 'angebot';
}
/**
* @inheritDoc
*/
public function getIndexName()
{
return 'offers';
}
/**
* @inheritDoc
*/
public function getIndexTitle()
{
return 'Angebote';
}
/**
* @inheritDoc
*/
protected function configureBaseQuery(SelectQuery $query)
{
$query
->cols([
'a.id',
'a.projekt',
'a.datum',
'a.belegnr',
'a.auftrag', // Auftragsnummer
'a.status',
'a.name',
'a.kundennummer',
'a.internebezeichnung',
'a.anfrage',
'a.gesamtsumme',
'a.waehrung',
])
->from('angebot AS a')
->where('a.belegnr != ?', '');
}
/**
* @inheritDoc
*/
protected function configureItemQuery(SelectQuery $baseQuery, $indexId)
{
$baseQuery->where('a.id = ?', (int)$indexId);
}
/**
* @inheritDoc
*/
protected function configureCountQuery(SelectQuery $baseQuery)
{
$baseQuery->cols(['COUNT(a.id)' => 'total_count']);
}
/**
* @inheritDoc
*/
protected function configureSinceQuery(SelectQuery $baseQuery, DateTimeInterface $since)
{
$baseQuery->where('a.logdatei > ?', $since->format('Y-m-d H:m:i'));
}
/**
* @inheritDoc
*/
protected function getRowFormatter()
{
return static function (array $row) {
$projectId = (int)$row['projekt'];
$documentDate = date('d.m.Y', strtotime($row['datum']));
$title = $row['belegnr'];
$link = sprintf('index.php?module=angebot&action=edit&id=%d', $row['id']);
$data = new IndexData($title, $link, $projectId);
$data->setSubTitle($row['name']);
$data->addAdditionalInfo($documentDate);
$data->addAdditionalInfo(ucfirst($row['status']));
$data->addAdditionalInfo(number_format($row['gesamtsumme'], 2, ',', '.') . ' ' . $row['waehrung']);
$data->addSearchWord('angebot');
$data->addSearchWord($row['belegnr']);
$data->addSearchWord($row['auftrag']);
$data->addSearchWord($row['name']);
$data->addSearchWord($row['kundennummer']);
$data->addSearchWord($row['internebezeichnung']);
$data->addSearchWord($row['anfrage']);
$identifier = new IndexIdentifier('offers', (int)$row['id']);
return new IndexItem($identifier, $data);
};
}
}
@@ -0,0 +1,117 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use DateTimeInterface;
use Xentral\Components\Database\SqlQuery\SelectQuery;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexData;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
final class OrderProvider extends AbstractBulkIndexDatabaseProvider
{
/**
* @inheritDoc
*/
public function getModuleName()
{
return 'auftrag';
}
/**
* @inheritDoc
*/
public function getIndexName()
{
return 'orders';
}
/**
* @inheritDoc
*/
public function getIndexTitle()
{
return 'Aufträge';
}
/**
* @inheritDoc
*/
protected function configureBaseQuery(SelectQuery $query)
{
$query
->cols([
'a.id',
'a.projekt',
'a.datum',
'a.belegnr',
'a.internet',
'a.angebot', // Angebotsnummer
'a.status',
'a.name',
'a.kundennummer',
'a.internebezeichnung',
'a.ihrebestellnummer',
'a.gesamtsumme',
'a.waehrung',
])
->from('auftrag AS a')
->where('a.belegnr != ?', '');
}
/**
* @inheritDoc
*/
protected function configureItemQuery(SelectQuery $baseQuery, $indexId)
{
$baseQuery->where('a.id = ?', (int)$indexId);
}
/**
* @inheritDoc
*/
protected function configureCountQuery(SelectQuery $baseQuery)
{
$baseQuery->cols(['COUNT(a.id)' => 'total_count']);
}
/**
* @inheritDoc
*/
protected function configureSinceQuery(SelectQuery $baseQuery, DateTimeInterface $since)
{
$baseQuery->where('a.logdatei > ?', $since->format('Y-m-d H:m:i'));
}
/**
* @inheritDoc
*/
protected function getRowFormatter()
{
return static function (array $row) {
$projectId = (int)$row['projekt'];
$documentDate = date('d.m.Y', strtotime($row['datum']));
$title = $row['belegnr'];
$link = sprintf('index.php?module=auftrag&action=edit&id=%d', $row['id']);
$data = new IndexData($title, $link, $projectId);
$data->setSubTitle($row['name']);
$data->addAdditionalInfo($documentDate);
$data->addAdditionalInfo(ucfirst($row['status']));
$data->addAdditionalInfo(number_format($row['gesamtsumme'], 2, ',', '.') . ' ' . $row['waehrung']);
$data->addSearchWord('auftrag');
$data->addSearchWord($row['belegnr']);
$data->addSearchWord($row['internet']);
$data->addSearchWord($row['angebot']);
$data->addSearchWord($row['name']);
$data->addSearchWord($row['kundennummer']);
$data->addSearchWord($row['internebezeichnung']);
$data->addSearchWord($row['ihrebestellnummer']);
$identifier = new IndexIdentifier('orders', (int)$row['id']);
return new IndexItem($identifier, $data);
};
}
}
@@ -0,0 +1,21 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
interface SearchIndexProviderInterface
{
/**
* @return string Nur Kleinbuchstaben erlaubt
*/
public function getIndexName();
/**
* @return string
*/
public function getIndexTitle();
/**
* @return string|null
*/
public function getModuleName();
}
@@ -0,0 +1,103 @@
<?php
namespace Xentral\Modules\SuperSearch\SearchIndex\Provider;
use DateTimeInterface;
use Xentral\Components\Database\SqlQuery\SelectQuery;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexData;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
final class TrackingNumberProvider extends AbstractBulkIndexDatabaseProvider
{
/**
* @inheritDoc
*/
public function getModuleName()
{
return 'lieferschein';
}
/**
* @inheritDoc
*/
public function getIndexName()
{
return 'trackingnumber';
}
/**
* @inheritDoc
*/
public function getIndexTitle()
{
return 'Trackingnummer';
}
/**
* @inheritDoc
*/
protected function configureBaseQuery(SelectQuery $query)
{
$query
->cols([
'v.id',
'v.tracking',
'v.lieferschein',
'v.projekt',
'v.versendet_am'
])
->from('versand AS v')
->where('v.tracking != ?', '');
}
/**
* @inheritDoc
*/
protected function configureItemQuery(SelectQuery $baseQuery, $indexId)
{
$baseQuery->where('v.id = ?', (int)$indexId);
}
/**
* @inheritDoc
*/
protected function configureCountQuery(SelectQuery $baseQuery)
{
$baseQuery->cols(['COUNT(v.id)' => 'total_count']);
}
/**
* @inheritDoc
*/
protected function configureSinceQuery(SelectQuery $baseQuery, DateTimeInterface $since)
{
$baseQuery->where('v.versendet_am > ?', $since->format('Y-m-d H:m:i'));
}
/**
* @inheritDoc
*/
protected function getRowFormatter()
{
return static function (array $row) {
$projectId = (int)$row['projekt'];
$versandDatum = date('d.m.Y', strtotime($row['versendet_am']));
$title = $row['tracking'];
$link = sprintf('index.php?module=lieferschein&action=edit&id=%d', $row['lieferschein']);
$data = new IndexData($title, $link, $projectId);
$data->addSearchWord('trackingnummer');
$data->addSearchWord($row['tracking']);
$data->setSubTitle('');
$data->addAdditionalInfo('Versendet: '.$versandDatum);
$identifier = new IndexIdentifier('trackingnumber', (int)$row['id']);
return new IndexItem($identifier, $data);
};
}
}
@@ -0,0 +1,138 @@
<?php
namespace Xentral\Modules\SuperSearch;
use DateTimeImmutable;
use DateTimeInterface;
use Exception;
use Xentral\Components\Database\Database;
use Xentral\Modules\SuperSearch\Exception\InvalidArgumentException;
use Xentral\Modules\SuperSearch\SearchEngine\SearchTermParser;
use Xentral\Widgets\SuperSearch\Result\ResultCollection;
use Xentral\Widgets\SuperSearch\Result\ResultGroup;
use Xentral\Widgets\SuperSearch\Result\ResultItem;
final class SuperSearchEngine
{
/** @var Database $db */
private $db;
/** @var SearchTermParser $searchTermParser */
private $searchTermParser;
/**
* @param Database $database
*/
public function __construct(Database $database)
{
$this->searchTermParser = new SearchTermParser();
$this->db = $database;
}
/**
* @param string $searchTerm
* @param array|null $projectIds Projekt-IDs die der Benutzer aufrufen darf;
* null = Keine Einschränkung (nur bei Admins)
* @param array|null $moduleNames Module die der Benutzer aufrufen darf;
* null = Keine Einschränkung (nur bei Admins)
* @param int $resultLimit Anzahl der Ergebnisse
*
* @throws InvalidArgumentException
*
* @return ResultCollection
*/
public function search($searchTerm, array $projectIds = null, array $moduleNames = null, $resultLimit = 30)
{
$resultLimit = (int)$resultLimit;
if ($resultLimit < 1) {
throw new InvalidArgumentException('Parameter value $resultLimit is invalid.');
}
$searchTerm = $this->searchTermParser->parse($searchTerm);
// Ergebnisse mit Projekt-ID 0 immer anzeigen (z.b. Appstore-Ergebnisse)
if (is_array($projectIds) && !in_array(0, $projectIds, true)) {
$projectIds[] = 0;
}
if (is_array($moduleNames) && !in_array('appstore', $moduleNames, true)) {
$moduleNames[] = 'appstore';
}
$sqlProjects = '';
$sqlModules = '';
$bindValues = [
'search_term' => $searchTerm,
'result_limit' => $resultLimit,
];
if ($projectIds !== null) {
$sqlProjects = ' AND sii.project_id IN (:project_ids) ';
$bindValues['project_ids'] = (array)$projectIds;
}
if ($moduleNames !== null) {
$sqlModules = ' AND (sig.module IN (:module_names) OR sig.module IS NULL) ';
$bindValues['module_names'] = (array)$moduleNames;
}
$sql =
"SELECT
sii.index_name, sii.index_id, sig.title AS `index_title`, sii.project_id,
sii.title, sii.subtitle, sii.additional_infos, sii.link, sii.search_words
FROM `supersearch_index_item` AS `sii`
INNER JOIN `supersearch_index_group` AS `sig` ON sii.index_name = sig.name
WHERE MATCH (sii.search_words) AGAINST (:search_term IN BOOLEAN MODE)
{$sqlProjects}
{$sqlModules}
AND sii.outdated = 0 AND sig.active = 1
LIMIT 0, :result_limit";
$data = $this->db->fetchAll($sql, $bindValues);
return $this->buildResultCollection($data);
}
/**
* @param array $data
*
* @return ResultCollection
*/
private function buildResultCollection($data)
{
$lastIndexUpdate = $this->getRecentIndexTime();
$results = new ResultCollection([], $lastIndexUpdate);
foreach ($data as $item) {
if (!$results->hasGroup($item['index_name'])) {
$results->addGroup(new ResultGroup($item['index_name'], $item['index_title']));
}
/** @var ResultGroup $group */
$group = $results->getGroup($item['index_name']);
$group->addItem(ResultItem::fromDbState($item));
}
return $results;
}
/**
* Liefert den Zeitpunkt wann der Index das letzte Mal aktualisiert wurde
*
* @return DateTimeInterface|null
*/
private function getRecentIndexTime()
{
$sql =
'SELECT MAX(GREATEST(IFNULL(sig.last_full_update, 1), IFNULL(sig.last_diff_update, 1))) AS `last_update`
FROM `supersearch_index_group` AS sig WHERE sig.active = 1';
$value = $this->db->fetchValue($sql);
if (empty($value)) {
return null;
}
try {
$dateTime = new DateTimeImmutable($value);
} catch (Exception $exception) {
return null;
}
return $dateTime;
}
}
@@ -0,0 +1,487 @@
<?php
namespace Xentral\Modules\SuperSearch;
use DateTimeImmutable;
use DateTimeInterface;
use Exception;
use Xentral\Components\Database\Database;
use Xentral\Components\Database\Exception\DatabaseExceptionInterface;
use Xentral\Modules\SuperSearch\Exception\InvalidReturnValueException;
use Xentral\Modules\SuperSearch\Exception\ProviderIncompatibleException;
use Xentral\Modules\SuperSearch\Exception\ProviderMissingException;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexIdentifier;
use Xentral\Modules\SuperSearch\SearchIndex\Data\IndexItem;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\BulkIndexProviderInterface;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\DiffIndexProviderInterface;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\FullIndexProviderInterface;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\ItemIndexProviderInterface;
use Xentral\Modules\SuperSearch\SearchIndex\Provider\SearchIndexProviderInterface;
final class SuperSearchIndexer
{
/** @var Database $db */
private $db;
/** @var array|SearchIndexProviderInterface[] $provider */
private $provider;
/**
* @param Database $database
* @param SearchIndexProviderInterface[]|array $provider Nur aktive Provider übergeben!
*/
public function __construct(Database $database, array $provider = [])
{
$this->db = $database;
$this->provider = $provider;
}
/**
* Gibt Meta-Information zu den Providern zurück; nur von aktiven Providern
*
* @return array
*/
public function getProviderMetaData()
{
$meta = [];
foreach ($this->provider as $provider) {
$meta[] = [
'name' => $provider->getIndexName(),
'title' => $provider->getIndexTitle(),
'module' => $provider->getModuleName(),
];
}
return $meta;
}
/**
* Tatsächliche Index-Größe ermitteln (gruppiert nach Index) (nur von aktiven Providern)
*
* @return array
*/
public function getProviderIndexSizesCurrent()
{
$sql =
'SELECT sig.name, COUNT(sii.id) AS `index_size`
FROM `supersearch_index_group` AS `sig`
LEFT JOIN `supersearch_index_item` AS `sii` ON sig.name = sii.index_name AND sii.outdated = 0
WHERE sig.active = 1
GROUP BY sig.name';
$indexSizes = $this->db->fetchPairs($sql);
// Fehlende Indexe ergänzen, falls Provider registriert ist aber noch nie gelaufen ist
foreach ($this->provider as $provider) {
$indexName = $provider->getIndexName();
if (!isset($indexSizes[$indexName])) {
$indexSizes[$indexName] = null;
}
}
ksort($indexSizes);
return $indexSizes;
}
/**
* Potentielle Index-Größe ermitteln (gruppiert nach Index) (nur von aktiven Providern)
*
* @return array
*/
public function getProviderIndexSizesPotential()
{
$indexSizes = [];
foreach ($this->provider as $provider) {
// Möglich Index-Größe beim Provider erfragen
$indexSizePotential = null;
if ($provider instanceof BulkIndexProviderInterface) {
$indexSizePotential = $provider->getTotalCount();
}
$indexSizes[$provider->getIndexName()] = $indexSizePotential;
}
ksort($indexSizes);
return $indexSizes;
}
/**
* @param string $name Index-Name
*
* @throws ProviderMissingException
* @throws InvalidReturnValueException
*
* @return void
*/
public function updateIndexFull($name)
{
/** @var FullIndexProviderInterface $provider */
$provider = $this->tryGetProviderByIndexName($name);
if ($provider === null) {
throw new ProviderMissingException(sprintf('Provider for index "%s" is missing', $name));
}
if ($provider instanceof BulkIndexProviderInterface) {
/** @var BulkIndexProviderInterface $provider */
$totalCount = $provider->getTotalCount();
if ($totalCount < 0) {
throw new InvalidReturnValueException(sprintf(
'Method %s::getTotalCount() returned an invalid value "%s". Total count must be a positive number.',
get_class($provider),
$totalCount
));
}
$itemsPerStep = $provider->getBulkSize();
$currentOffset = 0;
// Alle Index-Einträge als "veraltet" markieren
$this->markIndexAsOutdated($name);
do {
$items = $provider->getBulkItems($currentOffset, $itemsPerStep);
$this->db->beginTransaction();
foreach ($items as $item) {
$this->saveItem($item);
}
unset($items);
$this->db->commit();
$currentOffset += $itemsPerStep;
} while ($currentOffset < $totalCount);
// Full-Update-Zeitpunkt aktualisieren
$this->updateLastFullUpdateTime($name);
// Diff-Update-Zeitpunkt ebenfalls aktualisieren > Nächstes Diff-Update dann ab diesem Zeitpunkt
$this->updateLastDiffUpdateTime($name);
// Als "veraltet" markierte Index-Einträge löschen
$this->deleteOutdatedIndexItems($name);
}
if (
$provider instanceof FullIndexProviderInterface &&
!$provider instanceof BulkIndexProviderInterface
) {
$this->markIndexAsOutdated($name);
$items = $provider->getAllItems();
$this->db->beginTransaction();
foreach ($items as $item) {
$this->saveItem($item);
}
$this->db->commit();
$this->updateLastFullUpdateTime($name);
$this->deleteOutdatedIndexItems($name);
}
}
/**
* @param string $name Index-Name
* @param DateTimeInterface $since
*
* @throws ProviderMissingException
*
* @return void
*/
public function updateIndexSince($name, DateTimeInterface $since)
{
/** @var DiffIndexProviderInterface $provider */
$provider = $this->tryGetProviderByIndexName($name);
if ($provider === null) {
throw new ProviderMissingException(sprintf('Provider for index "%s" is missing', $name));
}
if (!$provider instanceof DiffIndexProviderInterface) {
return;
}
$items = $provider->getItemsSince($since);
$this->db->beginTransaction();
foreach ($items as $item) {
$this->saveItem($item);
}
$this->db->commit();
$this->updateLastDiffUpdateTime($name);
}
/**
* @param string $name
*
* @return DateTimeInterface|null
*/
public function getLastFullIndexTime($name)
{
$dateString = $this->db->fetchValue(
'SELECT sig.last_full_update FROM `supersearch_index_group` AS `sig` WHERE sig.name = :index_name',
['index_name' => (string)$name]
);
try {
if (!empty($dateString)) {
return new DateTimeImmutable($dateString);
}
} catch (Exception $exception) {
// nope - return null
}
return null;
}
/**
* @param string $name
*
* @return DateTimeInterface|null
*/
public function getLastDiffIndexTime($name)
{
$dateString = $this->db->fetchValue(
'SELECT sig.last_diff_update FROM `supersearch_index_group` AS `sig` WHERE sig.name = :index_name',
['index_name' => (string)$name]
);
try {
if (!empty($dateString)) {
return new DateTimeImmutable($dateString);
}
} catch (Exception $exception) {
// nope - return null
}
return null;
}
/**
* @param IndexIdentifier $identifier
*
* @return void
*/
public function deleteIndexItem(IndexIdentifier $identifier)
{
$sql =
'DELETE FROM `supersearch_index_item`
WHERE `index_name` = :index_name AND `index_id` = :index_id
LIMIT 1';
$bindValues = [
'index_name' => $identifier->getName(),
'index_id' => $identifier->getId(),
];
$this->db->perform($sql, $bindValues);
}
/**
* @param IndexIdentifier $identifier
*
* @throws ProviderMissingException
* @throws ProviderIncompatibleException
*
* @return void
*/
public function updateIndexItem(IndexIdentifier $identifier)
{
/** @var ItemIndexProviderInterface $provider */
$provider = $this->tryGetProviderByIndexName($identifier->getName());
if ($provider === null) {
throw new ProviderMissingException(sprintf(
'Provider for index "%s" is missing.', $identifier->getName()
));
}
if (!$provider instanceof ItemIndexProviderInterface) {
throw new ProviderIncompatibleException(sprintf(
'Provider for index "%s" is incompatible. Provider %s does not implement %s.',
$identifier->getName(),
get_class($provider),
ItemIndexProviderInterface::class
));
}
$item = $provider->getItem($identifier);
if ($item === null) {
return;
}
$this->saveItem($item);
}
/**
* Updates or creates an index item
*
* @param IndexItem $item
*
* @throws ProviderMissingException
*
* @return void
*/
private function saveItem(IndexItem $item)
{
$existingItemId = (int)$this->db->fetchValue(
'SELECT sii.id
FROM `supersearch_index_item` AS `sii`
WHERE sii.index_name = :index_name AND sii.index_id = :index_id',
[
'index_name' => $item->identifier->getName(),
'index_id' => $item->identifier->getId(),
]
);
if ($existingItemId > 0) {
$this->updateItem($item);
} else {
$this->createItem($item);
}
}
/**
* @param IndexItem $item
*
* @throws DatabaseExceptionInterface
*
* @return void
*/
private function updateItem(IndexItem $item)
{
$searchWords = '';
if (!empty($item->data->getWords())) {
$searchWords = implode(' | ', $item->data->getWords());
}
$additionalInfos = null;
if (!empty($item->data->getAdditionalInfos())) {
$additionalInfos = implode(' ## ', $item->data->getAdditionalInfos());
}
$sql =
'UPDATE `supersearch_index_item`
SET
`project_id` = :project_id,
`title` = :title,
`subtitle` = :subtitle,
`additional_infos` = :additional_infos,
`link` = :link,
`search_words` = :search_words,
`created_at` = `created_at`,
`updated_at` = NOW(),
`outdated` = 0
WHERE `index_name` = :index_name AND `index_id` = :index_id
LIMIT 1';
$bindValues = [
'index_name' => $item->identifier->getName(),
'index_id' => $item->identifier->getId(),
'project_id' => $item->data->getProjectId(),
'title' => $item->data->getTitle(),
'link' => $item->data->getLink(),
'subtitle' => $item->data->getSubTitle(),
'search_words' => $searchWords,
'additional_infos' => $additionalInfos,
];
$this->db->perform($sql, $bindValues);
}
/**
* @param IndexItem $item
*
* @return void
*/
private function createItem(IndexItem $item)
{
$searchWords = '';
if (!empty($item->data->getWords())) {
$searchWords = implode(' | ', $item->data->getWords());
}
$additionalInfos = null;
if (!empty($item->data->getAdditionalInfos())) {
$additionalInfos = implode(' ## ', $item->data->getAdditionalInfos());
}
$sql =
'INSERT INTO `supersearch_index_item`
(`index_name`, `index_id`, `project_id`, `title`, `subtitle`, `additional_infos`,
`link`, `search_words`, `outdated`, `created_at`, `updated_at`)
VALUES
(:index_name, :index_id, :project_id, :title, :subtitle, :additional_infos,
:link, :search_words, 0, NOW(), NULL)';
$bindValues = [
'index_name' => $item->identifier->getName(),
'index_id' => $item->identifier->getId(),
'project_id' => $item->data->getProjectId(),
'title' => $item->data->getTitle(),
'link' => $item->data->getLink(),
'subtitle' => $item->data->getSubTitle(),
'search_words' => $searchWords,
'additional_infos' => $additionalInfos,
];
$this->db->perform($sql, $bindValues);
}
/**
* Alle Einträge als veraltet markieren
*
* Beim Update/Insert eines Eintrags wird dieser wieder auf ungelöscht gestellt bevor er wirklich gelöscht wird.
*
* @param string $indexName
*
* @return void
*/
private function markIndexAsOutdated($indexName)
{
$sql = 'UPDATE `supersearch_index_item` SET `outdated` = 1 WHERE `index_name` = :index_name';
$this->db->perform($sql, ['index_name' => (string)$indexName]);
}
/**
* @param string $indexName
*
* @return void
*/
private function deleteOutdatedIndexItems($indexName)
{
$sql = 'DELETE FROM `supersearch_index_item` WHERE `index_name` = :index_name AND `outdated` = 1';
$this->db->perform($sql, ['index_name' => (string)$indexName]);
}
/**
* @param string $name
*
* @return SearchIndexProviderInterface|null
*/
private function tryGetProviderByIndexName($name)
{
foreach ($this->provider as $provider) {
if ($name === $provider->getIndexName()) {
return $provider;
}
}
return null;
}
/**
* @param string $name
*
* @return void
*/
private function updateLastFullUpdateTime($name)
{
$sql = 'UPDATE `supersearch_index_group` SET `last_full_update` = NOW() WHERE `name` = :index_name LIMIT 1';
$this->db->perform($sql, ['index_name' => (string)$name]);
}
/**
* @param string $name
*
* @return void
*/
private function updateLastDiffUpdateTime($name)
{
$sql = 'UPDATE `supersearch_index_group` SET `last_diff_update` = NOW() WHERE `name` = :index_name LIMIT 1';
$this->db->perform($sql, ['index_name' => (string)$name]);
}
}
@@ -0,0 +1,168 @@
<?php
namespace Xentral\Modules\SuperSearch;
use Xentral\Components\Database\Database;
use Xentral\Components\Database\Exception\DatabaseExceptionInterface;
final class SuperSearchService
{
/** @var Database $db */
private $db;
/** @var SuperSearchIndexer $indexer */
private $indexer;
/**
* @param Database $database
* @param SuperSearchIndexer $indexer
*/
public function __construct(Database $database, SuperSearchIndexer $indexer)
{
$this->db = $database;
$this->indexer = $indexer;
}
/**
* @param string $indexName
*
* @return bool
*/
public function existsIndex($indexName)
{
$sql = 'SELECT sig.id FROM `supersearch_index_group` AS `sig` WHERE sig.name = :index_name';
$check = (int)$this->db->fetchValue($sql, ['index_name' => (string)$indexName]);
return $check > 0;
}
/**
* @param string $indexName
* @param string $indexTitle
* @param string|null $moduleName
*
* @return void
*/
public function createIndex($indexName, $indexTitle, $moduleName = null)
{
$sql =
'INSERT INTO `supersearch_index_group`
(`id`, `name`, `title`, `module`, `active`, `last_full_update`, `last_diff_update`)
VALUES
(NULL, :index_name, :index_title, :module_name, 1, NULL, NULL)';
$this->db->perform($sql, [
'index_name' => (string)$indexName,
'index_title' => (string)$indexTitle,
'module_name' => $moduleName !== null ? (string)$moduleName : null,
]);
}
/**
* @param string $indexName
*
* @return void
*/
public function deleteIndex($indexName)
{
$this->db->beginTransaction();
try {
$sql = 'DELETE FROM `supersearch_index_group` WHERE `name` = :index_name LIMIT 1';
$this->db->perform($sql, ['index_name' => (string)$indexName]);
$sql = 'DELETE FROM `supersearch_index_item` WHERE `index_name` = :index_name';
$this->db->perform($sql, ['index_name' => (string)$indexName]);
$this->db->commit();
} catch (DatabaseExceptionInterface $exception) {
$this->db->rollBack();
throw $exception;
}
}
/**
* @param string $indexName
*
* @return void
*/
public function activateIndex($indexName)
{
$sql = 'UPDATE `supersearch_index_group` SET `active` = 1 WHERE `name` = :index_name LIMIT 1';
$this->db->perform($sql, ['index_name' => (string)$indexName]);
}
/**
* @param string $indexName
*
* @return void
*/
public function deactivateIndex($indexName)
{
$sql =
'UPDATE `supersearch_index_group`
SET `active` = 0, `last_full_update` = NULL, `last_diff_update` = NULL
WHERE `name` = :index_name
LIMIT 1';
$this->db->perform($sql, ['index_name' => (string)$indexName]);
// Index leeren
$sql = 'DELETE FROM `supersearch_index_item` WHERE `index_name` = :index_name';
$this->db->perform($sql, ['index_name' => (string)$indexName]);
}
/**
* @return bool
*/
public function isIndexEmpty()
{
$sql = 'SELECT COUNT(sii.id) AS total_count FROM `supersearch_index_item` AS `sii` WHERE sii.outdated = 0';
$check = (int)$this->db->fetchValue($sql);
return $check === 0;
}
/**
* @return array
*/
public function getIndexStats()
{
$sql =
'SELECT sig.name, sig.title, sig.module, sig.active, sig.last_full_update, sig.last_diff_update
FROM `supersearch_index_group` AS `sig`';
$groups = $this->db->fetchAll($sql);
// Fehlende Provider hinzufügen (Provider die noch nie liefen)
$groupNames = array_column($groups, 'name');
$meta = $this->indexer->getProviderMetaData();
foreach ($meta as $info) {
if (!in_array($info['name'], $groupNames, true)) {
$groups[] = [
'name' => $info['name'],
'title' => $info['title'],
'module' => $info['module'],
'active' => null,
'last_full_update' => null,
'last_diff_update' => null,
];
}
}
// Index-Statistik hinzufügen
$indexSizeCurrent = $this->indexer->getProviderIndexSizesCurrent();
$indexSizePotential = $this->indexer->getProviderIndexSizesPotential();
foreach ($groups as $indexName => &$group) {
$indexName = $group['name'];
$group['index_size_current'] = isset($indexSizeCurrent[$indexName]) ? $indexSizeCurrent[$indexName] : null;
$group['index_size_potential'] = isset($indexSizePotential[$indexName]) ? $indexSizePotential[$indexName] : null;
if ($group['active'] === 1) {
$group['active'] = true;
}
if ($group['active'] === 0) {
$group['active'] = false;
}
}
unset($group);
return $groups;
}
}
@@ -0,0 +1,134 @@
<?php
namespace Xentral\Modules\SuperSearch\SystemHealth;
use DateInterval;
use DateTime;
use DateTimeImmutable;
use Exception;
use Xentral\Components\Database\Database;
final class SuperSearchHealthChecker
{
/** @var Database $db */
private $db;
/**
* @param Database $database
*/
public function __construct(Database $database)
{
$this->db = $database;
}
/**
* @return array
*/
public function getHealthStatus()
{
$sql =
'SELECT
sig.id, sig.name, sig.title, sig.module,
IFNULL(sig.last_diff_update, sig.last_full_update) AS `last_cron_run`,
MIN(IFNULL(sii.updated_at, sii.created_at)) AS `oldest_index_item`
FROM `supersearch_index_group` AS sig
LEFT JOIN `supersearch_index_item` AS sii ON sig.name = sii.index_name AND sii.outdated = 0
WHERE sig.active = 1
GROUP BY sig.name';
$indexes = $this->db->fetchAll($sql);
$outdatedTime = new DateTime('now');
$outdatedTime->sub(new DateInterval('PT48H'));
foreach ($indexes as $index) {
$lastCronRun = $this->tryCreateDateTimeObject($index['last_cron_run']);
if ($lastCronRun === null) {
// Prozessstarter ist für diesen Index nie gelaufen
return $this->buildResultForNotRunningSheduler($index['name']);
}
if ($lastCronRun !== null) {
if ($lastCronRun < $outdatedTime) {
// Prozessstarter ist für diesen Index seit mehr als 48 Stunden nicht mehr gelaufen
return $this->buildResultForOutdatedShedulerRunDate($index['name']);
}
}
$oldestIndexItem = $this->tryCreateDateTimeObject($index['oldest_index_item']);
if ($oldestIndexItem !== null) {
if ($oldestIndexItem < $outdatedTime) {
// Such-Index enthält Einträge die seit mehr als 48 Stunden nicht mehr aktualisiert wurden
return $this->buildResultForOutdatedIndexItem($index['name']);
}
}
}
// Wenn Code bis hierhin gelauf ist, ist alles in Ordnung
return [
'type' => 'ok', // string [ok|warning|error]
'message' => null, // string|null
];
}
/**
* @param string $indexName
*
* @return array
*/
private function buildResultForNotRunningSheduler($indexName)
{
return [
'type' => 'error',
'message' => sprintf(
'Befüllung für Such-Index "%s" wurde noch nie ausgeführt.', $indexName
),
];
}
/**
* @param string $indexName
*
* @return array
*/
private function buildResultForOutdatedShedulerRunDate($indexName)
{
return [
'type' => 'error',
'message' => sprintf(
'Befüllung des Such-Index "%s" wurde seit mehr als 48 Stunden nicht mehr ausgeführt.', $indexName
),
];
}
/**
* @param string $indexName
*
* @return array
*/
private function buildResultForOutdatedIndexItem($indexName)
{
return [
'type' => 'warning',
'message' => sprintf(
'Der Such-Index "%s" enthält Einträge die seit mehr als 48 Stunden nicht mehr aktualisiert wurden.',
$indexName
),
];
}
/**
* @param string|null $dateTimeString
*
* @return DateTimeImmutable|null
*/
private function tryCreateDateTimeObject($dateTimeString = null)
{
try {
if (!empty($dateTimeString)) {
return new DateTimeImmutable($dateTimeString);
}
} catch (Exception $exception) {
}
return null;
}
}
@@ -0,0 +1,43 @@
<?php
namespace Xentral\Modules\SuperSearch\Wrapper;
use erpAPI;
/**
* Anti-Corruption-Layer für erp::GetKonfiguration und erp::SetKonfigurationValue
*/
final class CompanyConfigWrapper
{
/** @var erpAPI $erp */
private $erp;
/**
* @param erpAPI $erp
*/
public function __construct(erpAPI $erp)
{
$this->erp = $erp;
}
/**
* @param string $name
*
* @return mixed
*/
public function get($name)
{
return $this->erp->GetKonfiguration($name);
}
/**
* @param string $name
* @param mixed $value
*
* @return void
*/
public function set($name, $value)
{
$this->erp->SetKonfigurationValue($name, $value);
}
}
@@ -0,0 +1,124 @@
var SuperSearchUi = (function ($) {
'use strict';
var me = {
init: function () {
me.registerEvents();
},
registerEvents: function () {
$('#supersearch-fullindex-task-trigger').on('click', function (e) {
e.preventDefault();
me.onClickExecuteFullIndexTask();
});
$('.button-provider-activate').on('click', function (e) {
e.preventDefault();
var indexName = $(this).data('indexName');
me.onClickActivateProvider(indexName);
});
$('.button-provider-deactivate').on('click', function (e) {
e.preventDefault();
var indexName = $(this).data('indexName');
me.onClickDeactivateProvider(indexName);
});
},
onClickExecuteFullIndexTask: function () {
var $container = $('#supersearch-fullindex-task-wrapper');
$.ajax({
url: 'index.php?module=supersearch&action=settings',
data: {
cmd: 'run-full-index-task'
},
method: 'post',
dataType: 'json',
beforeSend: function () {
$container.loadingOverlay();
},
success: function () {
var message = '<div class="success">Such-Index wurde erfolgreich neu aufgebaut.</div>';
message += '<a class="button button-primary" href="index.php?module=supersearch&action=settings">';
message += 'Seite neuladen</a>';
$container.html(message);
},
error: function (jqXhr) {
var errorMessage = 'Unbekannter Fehler';
if (jqXhr.hasOwnProperty('responseJSON') && jqXhr.responseJSON.hasOwnProperty('error')) {
errorMessage = jqXhr.responseJSON.error;
}
var message = '<div class="warning">';
message += 'Fehler beim Aufbau des Such-Indexes: ';
message += errorMessage;
message += '</div>';
$container.html(message);
},
complete: function () {
$container.loadingOverlay('remove');
}
});
},
/**
* @param {string} indexName
*/
onClickActivateProvider: function (indexName) {
$.ajax({
url: 'index.php?module=supersearch&action=settings',
data: {
cmd: 'activate-provider',
index_name: indexName
},
method: 'post',
dataType: 'json',
success: function () {
window.location.reload();
},
error: function (jqXhr) {
var errorMessage = 'Unbekannter Fehler';
if (jqXhr.hasOwnProperty('responseJSON') && jqXhr.responseJSON.hasOwnProperty('error')) {
errorMessage = jqXhr.responseJSON.error;
}
alert('Fehler beim Aktivieren des Providers: ' + errorMessage);
}
});
},
/**
* @param {string} indexName
*/
onClickDeactivateProvider: function (indexName) {
$.ajax({
url: 'index.php?module=supersearch&action=settings',
data: {
cmd: 'deactivate-provider',
index_name: indexName
},
method: 'post',
dataType: 'json',
success: function () {
window.location.reload();
},
error: function (jqXhr) {
var errorMessage = 'Unbekannter Fehler';
if (jqXhr.hasOwnProperty('responseJSON') && jqXhr.responseJSON.hasOwnProperty('error')) {
errorMessage = jqXhr.responseJSON.error;
}
alert('Fehler beim Deaktivieren des Providers: ' + errorMessage);
}
});
}
};
return {
init: me.init
};
})(jQuery);
$(document).ready(function () {
SuperSearchUi.init();
});