at the end of the day, it was inevitable

This commit is contained in:
Mo Elzubeir
2022-12-09 08:36:26 -06:00
commit 1218570914
1768 changed files with 887087 additions and 0 deletions
@@ -0,0 +1,118 @@
<?php
namespace AppBundle\AdvancedFilters;
use AppBundle\AdvancedFilters\Aggregator\AFAggregatorInterface;
use AppBundle\Form\Type\AdvancedFilter\AdvancedFilterParameters;
use Common\Enum\AFTypeEnum;
use IndexBundle\Filter\Factory\FilterFactoryInterface;
use IndexBundle\SearchRequest\SearchRequestInterface;
/**
* Class AFResolver
*
* @package AppBundle\AdvancedFilters\Elasticsearch
*/
class AFResolver implements AFResolverInterface
{
/**
* @var AFAggregatorInterface
*/
private $aggregator;
/**
* @var FilterFactoryInterface
*/
private $factory;
/**
* AFResolver constructor.
*
* @param AFAggregatorInterface $aggregator A AFAggregatorInterface
* instance.
* @param FilterFactoryInterface $factory A FilterFactoryInterface
* instance.
*/
public function __construct(
AFAggregatorInterface $aggregator,
FilterFactoryInterface $factory
) {
$this->aggregator = $aggregator;
$this->factory = $factory;
}
/**
* Get Available values for specified filter or for all.
*
* @param SearchRequestInterface $request A SearchRequestInterface instance.
*
* @return array
*/
public function getAvailables(SearchRequestInterface $request)
{
//
// Return assoc array for all available filters with its values.
//
return array_map(function ($values) {
return [ 'data' => $values ];
}, $this->aggregator->getValues($request));
}
/**
* Generate proper FilterInterface instance for specified filter name.
*
* @param array $AFConfig Advanced filters configuration.
* @param string $name Filter name.
* @param AdvancedFilterParameters $params Filter value or value label.
*
* @return \IndexBundle\Filter\FilterInterface
*/
public function generateFilter(array $AFConfig, $name, AdvancedFilterParameters $params)
{
if (! isset($AFConfig[$name])) {
throw new \InvalidArgumentException("Unknown filter '{$name}'.");
}
$config = $AFConfig[$name];
$fieldName = $config['field_name'];
switch ($config['type']) {
//
// Additional query.
//
case AFTypeEnum::QUERY:
$filter = $params->createQueryFilter($config['names'], $this->factory);
break;
//
// Filter by range.
//
case AFTypeEnum::RANGE:
$ranges = $config['ranges'];
$filter = $params->createRangeFilter($fieldName, $ranges, $this->factory);
break;
//
// Filter by single value.
//
case AFTypeEnum::SIMPLE:
//
// NOTICE:
//
// We not validate given value so client may provide valid value
// for current filtered field but not existing in available filter
// values for current search request.
//
// In this case client will receive zero documents, so maybe
// validating is not necessary.
//
$filter = $params->createSimpleFilter($fieldName, $this->factory);
break;
default:
throw new \RuntimeException("Unsupported type {$config['type']}");
}
return $filter;
}
}
@@ -0,0 +1,43 @@
<?php
namespace AppBundle\AdvancedFilters;
use AppBundle\Form\Type\AdvancedFilter\AdvancedFilterParameters;
use Common\Enum\AFSourceEnum;
use IndexBundle\Filter\FilterInterface;
use IndexBundle\SearchRequest\SearchRequestInterface;
/**
* Interface AFResolverInterface
* Resolve concrete advanced filter.
*
* @package AppBundle\AdvancedFilters
*/
interface AFResolverInterface
{
/**
* Max advanced filters values per page for each filter.
*/
const MAX_VALUES = 10;
/**
* Get Available values for specified filter or for all.
*
* @param SearchRequestInterface $request A SearchRequestInterface instance.
*
* @return array
*/
public function getAvailables(SearchRequestInterface $request);
/**
* Generate proper FilterInterface instance for specified filter name.
*
* @param array $AFConfig Advanced filters configuration.
* @param string $name Filter name.
* @param AdvancedFilterParameters $params Filter value or value label.
*
* @return FilterInterface
*/
public function generateFilter(array $AFConfig, $name, AdvancedFilterParameters $params);
}
@@ -0,0 +1,211 @@
<?php
namespace AppBundle\AdvancedFilters;
use Common\Enum\AFSourceEnum;
use Common\Enum\DocumentsAFNameEnum;
use Common\Enum\AFTypeEnum;
use Common\Enum\FieldNameEnum;
use Common\Enum\SourcesAFNameEnum;
/**
* Class AdvancedFiltersConfig
* @package AppBundle\AdvancedFilters
*/
class AdvancedFiltersConfig
{
/**
* Configuration of advanced filter for sources.
*
* @var array[]
*/
private static $configs = [
//
// Configuration of advanced filter for documents.
//
AFSourceEnum::FEED => [
DocumentsAFNameEnum::ADDITIONAL_QUERY => [
'type' => AFTypeEnum::QUERY,
'description' => 'Additional specifying query.',
'field_name' => '',
'names' => [
FieldNameEnum::MAIN,
FieldNameEnum::TITLE,
],
],
DocumentsAFNameEnum::SOURCE => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::SOURCE_TITLE,
'description' => 'Filter documents by source title.',
],
DocumentsAFNameEnum::ARTICLE_DATE => [
'type' => AFTypeEnum::RANGE,
'field_name' => FieldNameEnum::PUBLISHED,
'ranges' => [
'15 Minutes' => [ 'from' => 'now-15m', 'key' => '15 Minutes' ],
'30 Minutes' => [ 'from' => 'now-30m', 'key' => '30 Minutes' ],
'1 Hour' => [ 'from' => 'now-1H', 'key' => '1 Hour' ],
'24 Hour' => [ 'from' => 'now-1d', 'key' => '24 Hour' ],
'7 Days' => [ 'from' => 'now-7d', 'key' => '7 Days' ],
],
'description' => 'Filter documents by found date.',
],
DocumentsAFNameEnum::SOURCE_COUNTRY => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::COUNTRY,
'description' => 'Filter documents by source country.',
],
DocumentsAFNameEnum::SOURCE_STATE => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::STATE,
'description' => 'Filter documents by source state.',
],
DocumentsAFNameEnum::SOURCE_CITY => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::CITY,
'description' => 'Filter documents by source city.',
],
// DocumentsAFNameEnum::SOURCE_SECTION => [
// 'type' => AFTypeEnum::SIMPLE,
// 'field_name' => FieldNameEnum::SECTION,
// 'description' => 'Filter documents by source section.',
// ],
DocumentsAFNameEnum::ARTICLE_LANGUAGE => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::LANG,
'description' => 'Filter documents by language.',
],
DocumentsAFNameEnum::AUTHOR => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::AUTHOR_NAME,
'description' => 'Filter documents by author name.',
],
DocumentsAFNameEnum::PUBLISHER => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::PUBLISHER,
'description' => 'Filter documents by publisher.',
],
DocumentsAFNameEnum::REACH => [
'type' => AFTypeEnum::RANGE,
'field_name' => FieldNameEnum::VIEWS,
'ranges' => [
'0+' => [ 'from' => 0, 'to' => 1000 ],
'1000+' => [ 'from' => 1000, 'to' => 5000 ],
'5000+' => [ 'from' => 5000, 'to' => 10000 ],
'10000+' => [ 'from' => 10000, 'to' => 25000 ],
'25000+' => [ 'from' => 25000, 'to' => 50000 ],
'50000+' => [ 'from' => 50000, 'to' => 100000 ],
'100000+' => [ 'from' => 100000, 'to' => 250000 ],
'250000+' => [ 'from' => 250000, 'to' => 500000 ],
'500000+' => [ 'from' => 500000, 'to' => 1000000 ],
'1000000+' => [ 'from' => 1000000, 'to' => 2500000 ],
'2500000+' => [ 'from' => 2500000, 'to' => 5000000 ],
'5000000+' => [ 'from' => 5000000, 'to' => 10000000 ],
'10000000+' => [ 'from' => 10000000 ],
],
'description' => 'Filter documents by views count.',
],
DocumentsAFNameEnum::SENTIMENT => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::SENTIMENT,
'description' => 'Filter documents by sentiment.',
],
],
//
// Configuration of advanced filter for sources.
//
AFSourceEnum::SOURCE => [
SourcesAFNameEnum::LANG => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::LANG,
'description' => 'Filter sources by language.',
],
SourcesAFNameEnum::COUNTRY => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::COUNTRY,
'description' => 'Filter sources by country.',
],
SourcesAFNameEnum::STATE => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::STATE,
'description' => 'Filter sources by state.',
],
SourcesAFNameEnum::CITY => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::CITY,
'description' => 'Filter sources by city.',
],
SourcesAFNameEnum::SECTION => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::SECTION,
'description' => 'Filter sources by section.',
],
SourcesAFNameEnum::MEDIA_TYPE => [
'type' => AFTypeEnum::SIMPLE,
'field_name' => FieldNameEnum::SOURCE_PUBLISHER_TYPE,
'description' => 'Filter sources by media type.',
],
],
];
/**
* Get configuration for specified document's source.
*
* @param string $name One of available constants from AFSourceEnum.
*
* @return array[]
*
* @see AFSourceEnum
*/
public static function getConfig($name)
{
if (! isset(self::$configs[$name])) {
throw new \InvalidArgumentException("Unknown config '{$name}'");
}
return self::$configs[$name];
}
/**
* Get default value for specified source.
*
* @param string $name One of available constants from AFSourceEnum.
*
* @return array[]
*
* @see AFSourceEnum
*/
public static function getDefault($name)
{
switch ($name) {
case AFSourceEnum::FEED:
return [
DocumentsAFNameEnum::SOURCE => [ 'data' => [] ],
DocumentsAFNameEnum::ARTICLE_DATE => [ 'data' => [] ],
DocumentsAFNameEnum::SOURCE_COUNTRY => [ 'data' => [] ],
DocumentsAFNameEnum::SOURCE_STATE => [ 'data' => [] ],
DocumentsAFNameEnum::SOURCE_CITY => [ 'data' => [] ],
// DocumentsAFNameEnum::SOURCE_SECTION => [ 'data' => [] ],
DocumentsAFNameEnum::ARTICLE_LANGUAGE => [ 'data' => [] ],
DocumentsAFNameEnum::AUTHOR => [ 'data' => [] ],
DocumentsAFNameEnum::PUBLISHER => [ 'data' => [] ],
DocumentsAFNameEnum::REACH => [ 'data' => [] ],
DocumentsAFNameEnum::SENTIMENT => [ 'data' => [] ],
];
case AFSourceEnum::SOURCE:
return [
SourcesAFNameEnum::LANG => [ 'data' => [] ],
SourcesAFNameEnum::COUNTRY => [ 'data' => [] ],
SourcesAFNameEnum::STATE => [ 'data' => [] ],
SourcesAFNameEnum::CITY => [ 'data' => [] ],
SourcesAFNameEnum::SECTION => [ 'data' => [] ],
SourcesAFNameEnum::MEDIA_TYPE => [ 'data' => [] ],
];
default:
throw new \InvalidArgumentException('Unknown source '. $name);
}
}
}
@@ -0,0 +1,25 @@
<?php
namespace AppBundle\AdvancedFilters\Aggregator;
use IndexBundle\SearchRequest\SearchRequestInterface;
/**
* Interface AFAggregatorInterface
*
* Advanced filters aggregator interface.
*
* @package AppBundle\AdvancedFilters\Aggregator
*/
interface AFAggregatorInterface
{
/**
* Return available filters values for specified request.
*
* @param SearchRequestInterface $request A SearchRequestInterface instance.
*
* @return array
*/
public function getValues(SearchRequestInterface $request);
}
@@ -0,0 +1,152 @@
<?php
namespace AppBundle\AdvancedFilters\Aggregator;
use Common\Enum\DocumentsAFNameEnum;
use Common\Enum\AFTypeEnum;
use IndexBundle\Aggregation\AggregationInterface;
use IndexBundle\Index\IndexInterface;
use IndexBundle\SearchRequest\SearchRequestInterface;
/**
* Class AbstractElasticSearchAFAggregator
*
* Realization of AFAggregatorInterface for Elasticsearch.
*
* @package AppBundle\AdvancedFilters\Aggregator
*/
abstract class AbstractElasticSearchAFAggregator implements AFAggregatorInterface
{
/**
* @var IndexInterface
*/
private $index;
/**
* ElasticsearchAFAggregator constructor.
*
* @param IndexInterface $index A IndexInterface instance.
*/
public function __construct(IndexInterface $index)
{
$this->index = $index;
}
/**
* Return available filters values for specified request.
*
* @param SearchRequestInterface $request A SearchRequestInterface instance.
*
* @return array
*
* @see AFSourceEnum
*/
public function getValues(SearchRequestInterface $request)
{
$aggregations = [];
$AFConfig = $this->getAggregationConfig();
foreach ($AFConfig as $aggregationName => $config) {
if ($config['type'] !== AFTypeEnum::QUERY) {
$aggregations[] = $this
->createAggregation($aggregationName, $config);
}
}
// Create new builder for aggregation only.
$builder = $this->index->createRequestBuilder();
$response = $builder
->fromSearchRequest($request)
->setAggregation($aggregations)
->setLimit(0) // We don't need any founded documents, only aggregations
// results.
->build()
->execute();
// Normalize aggregation results.
$results = $response->getAggregationResults();
$values = [];
foreach ($results as $name => $body) {
//
// Normalize concrete filter aggregation.
//
// For some reasons ElasticSearch invert aggregation data in buckets
// when we try to aggregate filed with 'date' type and our custom
// names from config are assigned to invalid values. So for 'articleDate'
// filter we invert all values in bucket.
//
if ($name === DocumentsAFNameEnum::ARTICLE_DATE) {
$body = array_reverse($body);
}
$values[$name] = [];
foreach ($body as $value) {
//
// Normalize concrete filter aggregation result.
//
$valueName = $value['value'];
$values[$name][] = [
'value' => $valueName,
'count' => $value['count'],
];
}
}
//
// Get not founded advanced filters values and force it into response.
//
$notFounded = array_diff(array_keys($this->getDefaultValue()), array_keys($results));
foreach ($notFounded as $filter) {
$values[$filter] = [];
}
return $values;
}
/**
* Create new Aggregation instance from specified config.
*
* @param string $name Aggregation name.
* @param array $config Aggregation config.
*
* @return AggregationInterface
*/
private function createAggregation($name, array $config)
{
$factory = $this->index->getAggregationFactory();
$aggregation = $this->index->getAggregation();
// Get aggregation type and convert to proper ElasticSearch aggregation type.
$type = ($config['type'] === AFTypeEnum::SIMPLE) ? 'terms' : 'range';
$params = [
'type' => $type,
'field_name' => $config['field_name'],
];
if ($type === AFTypeEnum::RANGE) {
// We should get only ranges without names.
$params['ranges'] = array_values($config['ranges']);
}
// Create new aggregation.
return $aggregation->getAggregation($name, $factory->{$type}($params));
}
/**
* Aggregation config.
*
* @return array
*/
abstract protected function getAggregationConfig();
/**
* Get default value for this aggregation results.
*
* @return array
*/
abstract protected function getDefaultValue();
}
@@ -0,0 +1,35 @@
<?php
namespace AppBundle\AdvancedFilters\Aggregator;
use AppBundle\AdvancedFilters\AdvancedFiltersConfig;
use Common\Enum\AFSourceEnum;
/**
* Class ArticleAFAggregator
*
* @package AppBundle\AdvancedFilters\Aggregator
*/
class ArticleAFAggregator extends AbstractElasticSearchAFAggregator
{
/**
* Aggregation config.
*
* @return array
*/
protected function getAggregationConfig()
{
return AdvancedFiltersConfig::getConfig(AFSourceEnum::FEED);
}
/**
* Get default value for this aggregation results.
*
* @return array
*/
protected function getDefaultValue()
{
return AdvancedFiltersConfig::getDefault(AFSourceEnum::FEED);
}
}
@@ -0,0 +1,65 @@
<?php
namespace AppBundle\AdvancedFilters\Aggregator;
use AppBundle\Entity\CacheItem;
use IndexBundle\SearchRequest\SearchRequestInterface;
use Psr\Cache\CacheItemPoolInterface;
/**
* Class CachedAFAggregator
*
* @package AppBundle\AdvancedFilters\Aggregator
*/
class CachedAFAggregator implements AFAggregatorInterface
{
const LIFETIME = 1800;
/**
* @var CacheItemPoolInterface
*/
private $cache;
/**
* @var AFAggregatorInterface
*/
private $internal;
/**
* CachedAFAggregator constructor.
*
* @param CacheItemPoolInterface $cache A CacheItemPoolInterface instance.
* @param AFAggregatorInterface $internal A AFAggregatorInterface instance.
*/
public function __construct(
CacheItemPoolInterface $cache,
AFAggregatorInterface $internal
) {
$this->cache = $cache;
$this->internal = $internal;
}
/**
* Return available filters values for specified request.
*
* @param SearchRequestInterface $request A SearchRequestInterface instance.
*
* @return array
*/
public function getValues(SearchRequestInterface $request)
{
$cachedValues = $this->cache->getItem($request->getHash());
if (! $cachedValues->isHit()) {
$cachedValues = new CacheItem(
$request->getHash(),
$this->internal->getValues($request),
self::LIFETIME
);
$this->cache->save($cachedValues);
}
return $cachedValues->get();
}
}
@@ -0,0 +1,40 @@
<?php
namespace AppBundle\AdvancedFilters\Aggregator;
use AppBundle\AdvancedFilters\AdvancedFiltersConfig;
use Common\Enum\AFSourceEnum;
use Common\Enum\DocumentsAFNameEnum;
use Common\Enum\AFTypeEnum;
use IndexBundle\Aggregation\AggregationInterface;
use IndexBundle\Index\IndexInterface;
use IndexBundle\SearchRequest\SearchRequestInterface;
/**
* Class SourceAFAggregator
*
* @package AppBundle\AdvancedFilters\Aggregator
*/
class SourceAFAggregator extends AbstractElasticSearchAFAggregator
{
/**
* Aggregation config.
*
* @return array
*/
protected function getAggregationConfig()
{
return AdvancedFiltersConfig::getConfig(AFSourceEnum::SOURCE);
}
/**
* Get default value for this aggregation results.
*
* @return array
*/
protected function getDefaultValue()
{
return AdvancedFiltersConfig::getDefault(AFSourceEnum::SOURCE);
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace AppBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* Class AppBundle
* @package AppBundle
*/
class AppBundle extends Bundle
{
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace AppBundle;
/**
* Class AppBundleServices
* @package AppBundle
*/
class AppBundleServices
{
/**
* Application configuration.
*
* Implements {@see \AppBundle\Configuration\ConfigurationInterface}
* interface.
*/
const CONFIGURATION = 'app.configuration';
/**
* Manager for sources.
*/
const SOURCE_MANAGER = 'app.source_manager';
}
@@ -0,0 +1,228 @@
<?php
namespace AppBundle\Cache;
use AppBundle\Entity\CacheItem;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
/**
* Class DoctrineCacheItemPool
*
* @package AppBundle\Cache
*/
class DoctrineCacheItemPool implements CacheItemPoolInterface
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var object[]
*/
private $deferred = [];
/**
* DoctrineCacheItemPool constructor.
*
* @param EntityManagerInterface $em A EntityManagerInterface instance.
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* Returns a Cache Item representing the specified key.
*
* This method must always return a CacheItemInterface object, even in case
* of a cache miss. It MUST NOT return null.
*
* @param string $key The key for which to return the corresponding Cache Item.
*
* @return CacheItemInterface
*/
public function getItem($key)
{
$item = $this->em->find(CacheItem::class, $key);
if (! $item instanceof CacheItem) {
$item = new CacheItem($key, null, null, false);
} elseif (time() > $item->getExpiresAt()) {
$this->em->remove($item);
$this->em->flush($item);
$item = new CacheItem($key, null, null, false);
}
$this->garbageCollector();
return $item;
}
/**
* Returns a traversable set of cache items.
*
* @param string[] $keys An indexed array of keys of items to retrieve.
*
* @return array|\Traversable
*/
public function getItems(array $keys = [])
{
$items = $this->em->getRepository(CacheItem::class)
->findBy([ 'key' => $keys ]);
$this->garbageCollector();
//
// todo add proper code here!
//
return $items;
}
/**
* Confirms if the cache contains specified cache item.
*
* Note: This method MAY avoid retrieving the cached value for performance
* reasons. This could result in a race condition with
* CacheItemInterface::get(). To avoid such situation use
* CacheItemInterface::isHit() instead.
*
* @param string $key The key for which to check existence.
*
* @return boolean
*/
public function hasItem($key)
{
return $this->getItem($key)->isHit();
}
/**
* Deletes all items in the pool.
*
* @return boolean
*/
public function clear()
{
/** @var EntityRepository $repository */
$repository = $this->em->getRepository(CacheItem::class);
$repository->createQueryBuilder('Item')
->delete()
->getQuery()
->execute();
return true;
}
/**
* Removes the item from the pool.
*
* @param string $key The key to delete.
*
* @return boolean
*/
public function deleteItem($key)
{
$this->garbageCollector();
return $this->deleteItems([ $key ]);
}
/**
* Removes multiple items from the pool.
*
* @param string[] $keys An array of keys that should be removed from the pool.
*
* @return boolean
*/
public function deleteItems(array $keys)
{
/** @var EntityRepository $repository */
$repository = $this->em->getRepository(CacheItem::class);
$repository->createQueryBuilder('Item')
->delete()
->where('Item.key IN ('. implode(', ', \nspl\a\map(function ($key) {
return "'{$key}'";
}, $keys)) .')')
->getQuery()
->execute();
$this->garbageCollector();
return true;
}
/**
* Persists a cache item immediately.
*
* @param CacheItemInterface $item The cache item to save.
*
* @return boolean
*/
public function save(CacheItemInterface $item)
{
if (! $item instanceof CacheItem) {
return false;
}
$this->em->persist($item);
$this->em->flush($item);
$this->garbageCollector();
return true;
}
/**
* Sets a cache item to be persisted later.
*
* @param CacheItemInterface $item The cache item to save.
*
* @return boolean
*/
public function saveDeferred(CacheItemInterface $item)
{
$this->deferred[] = $item;
return true;
}
/**
* Persists any deferred cache items.
*
* @return boolean
*/
public function commit()
{
foreach ($this->deferred as $item) {
$this->em->persist($item);
}
$this->em->flush($this->deferred);
$this->deferred = [];
$this->garbageCollector();
return true;
}
/**
* @return void
*/
private function garbageCollector()
{
if (mt_rand(0, 10) <= 1) {
$this->em->createQueryBuilder()
->delete()
->from(CacheItem::class, 'Item')
->where('Item.expiresAt < CURRENT_TIMESTAMP()')
->getQuery()
->execute();
}
}
}
@@ -0,0 +1,90 @@
<?php
namespace AppBundle\Command;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\LockHandler;
/**
* Class AbstractSingleCopyCommand
*
* Base class for commands which should be run in single instance at time.
*
* @package AppBundle\Command
*/
abstract class AbstractSingleCopyCommand extends Command
{
/**
* @var LoggerInterface
*/
private $logger;
/**
* AbstractSingleCopyCommand constructor.
*
* @param string $name Command name.
* @param LoggerInterface $logger A LoggerInterface instabce.
*/
public function __construct($name, LoggerInterface $logger)
{
parent::__construct($name);
$this->logger = $logger;
}
/**
* Executes the current command.
*
* This method is not abstract because you can use this class
* as a concrete class. In this case, instead of defining the
* execute() method, you set the code to execute by passing
* a Closure to the setCode() method.
*
* @param InputInterface $input An InputInterface instance.
* @param OutputInterface $output An OutputInterface instance.
*
* @return null|integer null or 0 if everything went fine, or an error code.
*
* @see setCode()
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$lock = new LockHandler($this->getName());
if (!$lock->lock()) {
$output->writeln(sprintf(
'Command \'%s\' is already executing.',
$this->getName()
));
return 1;
}
try {
$result = $this->doExecute($input, $output);
} catch (\Exception $exception) {
$this->logger->critical(sprintf(
'Command \'%s\' got exception \'%s\' while executing. %s',
$this->getName(),
get_class($exception),
$exception->getMessage()
));
$result = 127;
} finally {
$lock->release();
}
return $result;
}
/**
* @param InputInterface $input An InputInterface instance.
* @param OutputInterface $output An OutputInterface instance.
*
* @return null|integer
*/
abstract protected function doExecute(InputInterface $input, OutputInterface $output);
}
@@ -0,0 +1,66 @@
<?php
namespace AppBundle\Command;
use AppBundle\Manager\Source\SourceManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class FetchSourcesCommand
* @package AppBundle\Command
*/
class FetchSourcesCommand extends AbstractSingleCopyCommand
{
/**
* Command name.
*/
const NAME = 'socialhose:sources:fetch';
/**
* @var SourceManagerInterface
*/
private $manager;
/**
* FetchSourcesCommand constructor.
*
* @param SourceManagerInterface $manager A SourceManagerInterface instance.
* @param LoggerInterface $logger A LoggerInterface instance.
*/
public function __construct(
SourceManagerInterface $manager,
LoggerInterface $logger
) {
parent::__construct(self::NAME, $logger);
$this->manager = $manager;
}
/**
* Configures the current command.
*
* @return void
*/
protected function configure()
{
$this->setDescription('Fetch all sources from external index.');
}
/**
* @param InputInterface $input An InputInterface instance.
* @param OutputInterface $output An OutputInterface instance.
*
* @return null|integer
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$this->manager->pullFromExternal();
return 0;
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
namespace AppBundle\Command;
use IndexBundle\Index\External\InternalHoseIndex;
use IndexBundle\Model\Generator\ExternalDocumentGenerator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class GenerateCommand
*
* This command generate random documents and put it into demo elasticsearch
* server.
*
* @package AppBundle\Command
*/
class GenerateCommand extends Command
{
/**
* Command name.
*/
const NAME = 'socialhose:generate';
/**
* @var InternalHoseIndex
*/
private $index;
/**
* GenerateCommand constructor.
*
* @param InternalHoseIndex $index A InternalHoseIndex instance.
*/
public function __construct(InternalHoseIndex $index)
{
parent::__construct(self::NAME);
$this->index = $index;
}
/**
* Configures the current command.
*
* @return void
*/
protected function configure()
{
$this
->setDescription('Generate random documents. Use only for development.')
->addOption(
'count',
'c',
InputOption::VALUE_REQUIRED,
'How much document create',
10
);
}
/**
* Executes the current command.
*
* This method is not abstract because you can use this class
* as a concrete class. In this case, instead of defining the
* execute() method, you set the code to execute by passing
* a Closure to the setCode() method.
*
* @param InputInterface $input An InputInterface instance.
* @param OutputInterface $output An OutputInterface instance.
*
* @return null|integer null or 0 if everything went fine, or an error code.
*
* @throws \LogicException When this abstract method is not implemented.
*
* @see setCode()
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$generator = new ExternalDocumentGenerator();
$count = $input->getOption('count');
$documents = [];
for ($i = 0; $i < $count; ++$i) {
$documents[] = $generator->generate();
}
$this->index->index($documents);
return 0;
}
}
@@ -0,0 +1,278 @@
<?php
namespace AppBundle\Command;
use AppBundle\Utils\Purger\TruncateORMPurger;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\ORM\EntityManagerInterface;
use IndexBundle\Fixture\Executor\Factory\IndexFixtureExecutorFactory;
use IndexBundle\Fixture\Loader\IndexFixtureLoader;
use IndexBundle\Index\External\InternalHoseIndex;
use IndexBundle\Index\Internal\InternalIndexInterface;
use IndexBundle\Index\Source\SourceIndexInterface;
use IndexBundle\Util\Initializer\ExternalIndexInitializer;
use IndexBundle\Util\Initializer\InternalIndexInitializer;
use IndexBundle\Util\Initializer\SourceIndexInitializer;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader as DataFixturesLoader;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\KernelInterface;
/**
* Class LoadDataFixturesCommand
* @package AppBundle\Command
*/
class LoadDataFixturesCommand extends AbstractSingleCopyCommand
{
/**
* Command name.
*/
const NAME = 'socialhose:fixtures:load';
/**
* @var KernelInterface
*/
private $kernel;
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var InternalHoseIndex
*/
private $externalIndex;
/**
* @var InternalIndexInterface
*/
private $internalIndex;
/**
* @var SourceIndexInterface
*/
private $sourceIndex;
/**
* @var ContainerInterface
*/
private $container;
/**
* LoadDataFixturesCommand constructor.
* @param KernelInterface $kernel A KernelInterface instance.
* @param EntityManagerInterface $em A EntityManagerInterface
* instance.
* @param InternalHoseIndex $externalIndex A HoseIndex instance.
* @param InternalIndexInterface $internalIndex A InternalIndexInterface
* instance.
* @param SourceIndexInterface $sourceIndex A SourceIndexInterface
* instance.
* @param ContainerInterface $container A ContainerInterface instance.
* @param LoggerInterface $logger A LoggerInterface instance.
*/
public function __construct(
KernelInterface $kernel,
EntityManagerInterface $em,
InternalHoseIndex $externalIndex,
InternalIndexInterface $internalIndex,
SourceIndexInterface $sourceIndex,
ContainerInterface $container,
LoggerInterface $logger
) {
parent::__construct(self::NAME, $logger);
$this->kernel = $kernel;
$this->em = $em;
$this->externalIndex = $externalIndex;
$this->internalIndex = $internalIndex;
$this->sourceIndex = $sourceIndex;
$this->container = $container;
}
/**
* Configures the current command.
*
* @return void
*/
protected function configure()
{
$this
->setName(self::NAME)
->setDescription('Load database and index fixtures.')
->addOption('force', null, InputOption::VALUE_NONE)
->addOption(
'without-index',
null,
InputOption::VALUE_NONE,
'Do not load index fixtures.'
)
->addOption(
'without-database',
null,
InputOption::VALUE_NONE,
'Do not load database fixtures.'
);
}
/**
* @param InputInterface $input An InputInterface instance.
* @param OutputInterface $output An OutputInterface instance.
*
* @return null|integer
*/
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$withoutDatabase = $input->getOption('without-database');
$withoutIndex = $input->getOption('without-index');
if ($withoutDatabase && $withoutIndex) {
return 0;
}
if (! $input->getOption('force')) {
// Because this command can rebuild all index we use this option as
// as flag in order to prevent accidental run.
$message = 'Provide --force option if you really want to initialize index.';
$output->writeln($message);
return 0;
}
if (! $input->getOption('no-interaction') && ! $this->confirm($input, $output)) {
return 0;
}
// Get list of available data fixtures paths.
$paths = $this->getFixturesPaths();
// Load database fixtures.
if (! $withoutDatabase) {
$this->loadDatabase($output, $paths);
}
// Load index fixtures.
if (! $withoutIndex) {
$this->loadIndex($output, $paths);
}
return 0;
}
/**
* @return array
*/
private function getFixturesPaths()
{
$paths = [];
foreach ($this->kernel->getBundles() as $bundle) {
$path = $bundle->getPath() . '/DataFixtures/';
if (is_dir($path)) {
$paths[] = $path;
}
}
return $paths;
}
/**
* @param InputInterface $input A InputInterface instance.
* @param OutputInterface $output A OutputInterface instance.
*
* @return boolean
*/
private function confirm(InputInterface $input, OutputInterface $output)
{
/** @var \Symfony\Component\Console\Helper\SymfonyQuestionHelper $helper */
$helper = $this->getHelper('question');
$output->writeln('');
$output->writeln('<comment>All data will be purged.</comment>');
$question = new ConfirmationQuestion('Are you sure (y/N)? ', false);
return (boolean) $helper->ask($input, $output, $question);
}
/**
* @param OutputInterface $output A OutputInterface instance.
* @param array $paths Array of fixtures directories path.
*
* @return void
*/
private function loadDatabase(OutputInterface $output, array $paths)
{
//
// Purge internal tables.
//
$this->em->getConnection()->executeQuery('TRUNCATE internal_notification_scheduling');
$output->writeln('<comment>Load database fixtures:</comment>');
$loader = new DataFixturesLoader($this->container);
foreach ($paths as $path) {
$loader->loadFromDirectory($path);
}
$fixtures = $loader->getFixtures();
if (!$fixtures) {
throw new \InvalidArgumentException(
sprintf('Could not find any fixtures to load in: %s', "\n\n- " . implode("\n- ", $paths))
);
}
$purger = new TruncateORMPurger(new ORMPurger($this->em));
$purger->purge();
$executor = new ORMExecutor($this->em);
$executor->setLogger(function ($message) use ($output) {
$output->writeln(sprintf(' <comment>></comment> <info>%s</info>', $message));
});
$executor->execute($fixtures, true);
}
/**
* @param OutputInterface $output A OutputInterface instance.
* @param array $paths Array of fixtures directories path.
*
* @return void
*/
private function loadIndex(OutputInterface $output, array $paths)
{
$output->writeln('<comment>Load index fixtures:</comment>');
$loader = new IndexFixtureLoader($this->container);
foreach ($paths as $path) {
$loader->loadFromDirectory($path);
}
$fixtures = $loader->getFixtures();
// Purge indexes.
ExternalIndexInitializer::initialize($this->externalIndex);
InternalIndexInitializer::initialize($this->internalIndex);
SourceIndexInitializer::initialize($this->sourceIndex);
$executorFactory = new IndexFixtureExecutorFactory();
$executorFactory->external($this->externalIndex)
->setLogger(function ($message) use ($output) {
$output->writeln(sprintf(' <comment>></comment> <info>%s</info>', $message));
})
->execute($fixtures);
$executorFactory->internal($this->internalIndex)
->setLogger(function ($message) use ($output) {
$output->writeln(sprintf(' <comment>></comment> <info>%s</info>', $message));
})
->execute($fixtures);
$executorFactory->source($this->sourceIndex)
->setLogger(function ($message) use ($output) {
$output->writeln(sprintf(' <comment>></comment> <info>%s</info>', $message));
})
->execute($fixtures);
}
}
@@ -0,0 +1,273 @@
<?php
namespace AppBundle\Command;
use Common\Enum\FieldNameEnum;
use Elasticsearch\Client;
use Elasticsearch\ClientBuilder;
use IndexBundle\Index\Strategy\HoseIndexStrategy;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class ReindexDocumentsCommand
* @package AppBundle\Command
*/
class ReindexDocumentsCommand extends AbstractSingleCopyCommand
{
/**
* Command name.
*/
const NAME = 'socialhose:reindex:documents';
/**
* Size of document which is fetched for reindex.
*/
const BUCKET_SIZE = 10;
/**
* Name of the file where command should store idx of failed bucket.
*/
const FAILED_IDX_FILE = 'document_reindex_fail_idx';
/**
* @var string
*/
private $host;
/**
* @var integer
*/
private $port;
/**
* @var string
*/
private $varPath;
/**
* @var HoseIndexStrategy
*/
private $strategy;
/**
* FetchSourcesCommand constructor.
*
* @param LoggerInterface $logger A LoggerInterface instance.
* @param string $host A elasticsearch host name.
* @param string $port A elasticsearch port.
* @param string $varPath Path to directory where failed idx file
* should stored.
*/
public function __construct(
LoggerInterface $logger,
$host,
$port,
$varPath
) {
parent::__construct(self::NAME, $logger);
$this->host = $host;
$this->port = $port;
$this->varPath = realpath($varPath);
if ($this->varPath === false) {
throw new \InvalidArgumentException(sprintf(
'$varPath value \'%s\' is invalid path.',
$varPath
));
}
if (! is_dir($this->varPath)) {
throw new \InvalidArgumentException(sprintf(
'$varPath value \'%s\' is not a directory.',
$varPath
));
}
if (! is_writable($this->varPath)) {
throw new \InvalidArgumentException(sprintf(
'$varPath value \'%s\' is not available for writing.',
$varPath
));
}
}
/**
* Configures the current command.
*
* @return void
*/
protected function configure()
{
$this
->setDescription('Migrate documents from one document index to another')
->addArgument('src', InputArgument::REQUIRED, 'Source index name')
->addArgument('dest', InputArgument::REQUIRED, 'Destination index name');
}
/**
* @param InputInterface $input An InputInterface instance.
* @param OutputInterface $output An OutputInterface instance.
*
* @return null|integer
*
* @throws \Exception If can't reindex documents.
*/
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$currentBucketIdx = null;
try {
$destIndex = $input->getArgument('dest');
$client = ClientBuilder::create()
->setHosts([
[
'host' => $this->host,
'port' => $this->port,
],
])
->build();
$response = $client->search([
'body' => ['query' => ['match_all' => (object) []]],
'index' => $input->getArgument('src'),
'type' => 'document',
'scroll' => '1m',
'size' => self::BUCKET_SIZE,
]);
$totalBucketCount = $response['hits']['total'] / self::BUCKET_SIZE;
$scrollId = $response['_scroll_id'];
$currentBucketIdx = $this->getCurrentBucketIdx();
//
// Scroll required number of bucket.
//
// We can't use offset 'cause ElasticSearch are limiting max allowed
// offset value. Also because of it we use scroll api instead of just
// make search with offset.
//
for ($i = 0; $i < $currentBucketIdx; ++$i) {
$response = $client->scroll([ 'scroll_id' => $scrollId ]);
}
//
// Reindex all documents.
//
while ($this->indexDocuments($response['hits']['hits'], $client, $destIndex)) {
$output->writeln(sprintf(
'Process %d from %d buckets',
$currentBucketIdx,
$totalBucketCount
));
$response = $client->scroll([
'scroll_id' => $scrollId,
'scroll' => '1m',
]);
$currentBucketIdx++;
}
if (file_exists($this->varPath . DIRECTORY_SEPARATOR . self::FAILED_IDX_FILE)) {
unlink($this->varPath . DIRECTORY_SEPARATOR . self::FAILED_IDX_FILE);
}
} catch (\Exception $exception) {
if ($currentBucketIdx !== null) {
file_put_contents(
$this->varPath . DIRECTORY_SEPARATOR . self::FAILED_IDX_FILE,
$currentBucketIdx
);
}
throw $exception;
}
return 0;
}
/**
* @param array $documents Array of raw documents.
* @param Client $client A ElasticSearch Client instance.
* @param string $index A index name.
*
* @return boolean
*/
private function indexDocuments(array $documents, Client $client, $index)
{
if (count($documents) === 0) {
return false;
}
// We should split documents into several buckets 'cause we may exceed allowed
// request size for ElasticSearch (10mb for AWS instance).
$buckets = [];
$idx = 0;
$count = 0;
foreach ($documents as $document) {
if (++$count > self::BUCKET_SIZE / 2) {
$idx++;
$count = 0;
}
$data = $document['_source'];
if (isset($data['collection_id'])) {
$data[FieldNameEnum::COLLECTION_ID] = $data['collection_id'];
$data[FieldNameEnum::COLLECTION_TYPE] = $data['collection_type'];
}
if (isset($data['deleted_from'])) {
$data[FieldNameEnum::DELETE_FROM] = $data['deleted_from'];
}
if (! isset($data[FieldNameEnum::DELETE_FROM])) {
$data[FieldNameEnum::DELETE_FROM] = [];
}
$buckets[$idx][] = [
'index' => [
'_index' => $index,
'_type' => 'document',
],
];
$buckets[$idx][] = $this->getStrategy()->getIndexableData($data);
}
foreach ($buckets as $bucket) {
$client->bulk(['body' => $bucket]);
}
return true;
}
/**
* @return HoseIndexStrategy
*/
private function getStrategy()
{
if ($this->strategy === null) {
$this->strategy = new HoseIndexStrategy();
}
return $this->strategy;
}
/**
* @return integer
*/
private function getCurrentBucketIdx()
{
$filePath = $this->varPath . DIRECTORY_SEPARATOR . self::FAILED_IDX_FILE;
if (file_exists($filePath)) {
return (int) file_get_contents($filePath);
}
return 0;
}
}
@@ -0,0 +1,72 @@
<?php
namespace AppBundle\Command;
use AppBundle\Configuration\ConfigurationInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class SyncSiteConfigCommand
* @package AppBundle\Command
*/
class SyncSiteConfigCommand extends Command
{
const NAME = 'socialhose:site-settings:sync';
/**
* @var ConfigurationInterface
*/
private $configuration;
/**
* SyncSiteConfigCommand constructor.
*
* @param ConfigurationInterface $configuration A ConfigurationInterface
* instance.
*/
public function __construct(ConfigurationInterface $configuration)
{
parent::__construct(self::NAME);
$this->configuration = $configuration;
}
/**
* Configures the current command.
*
* @return void
*/
protected function configure()
{
$this->setDescription('Sync site settings with base');
}
/**
* Executes the current command.
*
* This method is not abstract because you can use this class
* as a concrete class. In this case, instead of defining the
* execute() method, you set the code to execute by passing
* a Closure to the setCode() method.
*
* @param InputInterface $input An InputInterface instance.
* @param OutputInterface $output An OutputInterface instance.
*
* @return null|integer null or 0 if everything went fine, or an error code.
*
* @throws \LogicException When this abstract method is not implemented.
*
* @see setCode()
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->configuration->syncWithDefinitions();
return 0;
}
}
@@ -0,0 +1,106 @@
<?php
namespace AppBundle\Command;
use CacheBundle\Entity\Query\StoredQuery;
use CacheBundle\Repository\StoredQueryRepository;
use Doctrine\ORM\EntityManagerInterface;
use OldSound\RabbitMqBundle\RabbitMq\ProducerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class UpdateStoredQueriesCommand
* @package AppBundle\Command
*/
class UpdateStoredQueriesCommand extends Command
{
const NAME = 'socialhose:stored-query:update';
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var ProducerInterface
*/
private $producer;
/**
* UpdateStoredQueriesCommand constructor.
*
* @param EntityManagerInterface $em A EntityManagerInterface instance.
* @param ProducerInterface $producer A ProducerInterface instance.
*/
public function __construct(
EntityManagerInterface $em,
ProducerInterface $producer
) {
parent::__construct(self::NAME);
$this->em = $em;
$this->producer = $producer;
}
/**
* Configures the current command.
*
* @return void
*/
protected function configure()
{
$this->setDescription('Update stored queries.');
}
/**
* Executes the current command.
*
* This method is not abstract because you can use this class
* as a concrete class. In this case, instead of defining the
* execute() method, you set the code to execute by passing
* a Closure to the setCode() method.
*
* @param InputInterface $input An InputInterface instance.
* @param OutputInterface $output An OutputInterface instance.
*
* @return null|integer null or 0 if everything went fine, or an error code.
*
* @throws \LogicException When this abstract method is not implemented.
*
* @see setCode()
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$queries = $this->getQueries();
/** @var StoredQuery $query */
foreach ($queries as $query) {
$this->producer->publish($query->getId());
}
return 0;
}
/**
* @return \Generator
*/
private function getQueries()
{
/** @var StoredQueryRepository $repository */
$repository = $this->em->getRepository(StoredQuery::class);
$iterate = $repository->getForUpdating()->getQuery()->iterate();
foreach ($iterate as $query) {
$query = $query[0];
yield $query;
$this->em->detach($query);
}
}
}
@@ -0,0 +1,212 @@
<?php
namespace AppBundle\Configuration;
/**
* Class AbstractConfiguration
* @package AppBundle\Configuration
*/
abstract class AbstractConfiguration implements ConfigurationInterface
{
/**
* @var ConfigurationParameterInterface[]
*/
private $map = [];
/**
* Array of changed parameters name.
*
* @var string[]
*/
private $changed = [];
/**
* Array of removed parameters name.
*
* @var string[]
*/
private $removed = [];
/**
* @var ConfigurationDefinitionMap
*/
protected $definitions;
/**
* AbstractConfiguration constructor.
*
* @param ConfigurationDefinitionMap $definitions A ConfigurationDefinitionMap
* instance.
*/
public function __construct(ConfigurationDefinitionMap $definitions)
{
$this->syncParameters();
$this->definitions = $definitions;
}
/**
* Get parameter value by name.
*
* @param string $name Parameter name.
* @param mixed $default Default value if parameter not found.
*
* @return mixed
*/
public function getParameter($name, $default = null)
{
if (! isset($this->map[$name]) || isset($this->removed[$name])) {
return $default;
}
$param = $this->map[$name];
if ($param === null) {
return $default;
}
$value = $param->getValue();
settype($value, $this->definitions->getDefinition($name)['type']);
return $value;
}
/**
* Sync current parameters with database.
*
* @return void
*/
public function syncParameters()
{
$params = $this->loadData();
foreach ($params as $param) {
$this->map[$param->getName()] = $param;
}
}
/**
* Get all available parameters.
*
* @return ConfigurationParameterInterface[]
*/
public function getParameters()
{
return $this->map;
}
/**
* Get parameter value by name.
*
* @param string $name Parameter name.
* @param mixed $value New parameter value.
*
* @return void
*/
public function setParameter($name, $value)
{
$this->map[$name]->setValue($this->definitions->normalize($name, $value));
$this->changed[$name] = true;
}
/**
* Set parameters.
*
* @param array $params Array where key is parameter name and value is new
* value.
*
* @return void
*/
public function setParameters(array $params)
{
foreach ($params as $name => $newValue) {
$this->setParameter($name, $newValue);
}
}
/**
* Sync configuration with storage.
*
* @return void
*/
public function sync()
{
$changed = \nspl\a\filter(function (ConfigurationParameterInterface $parameter) {
return isset($this->changed[$parameter->getName()]);
}, $this->map);
$removed = \nspl\a\filter(function (ConfigurationParameterInterface $parameter) {
return isset($this->removed[$parameter->getName()]);
}, $this->map);
if ((count($changed) === 0) && (count($removed) === 0)) {
return;
}
$this->doSync($changed, $removed);
$this->map = \nspl\a\filter(function (ConfigurationParameterInterface $parameter) {
return ! isset($this->removed[$parameter->getName()]);
}, $this->map);
$this->changed = [];
$this->removed = [];
}
/**
* Sync parameters with list of available.
*
* @return void
*/
public function syncWithDefinitions()
{
$notExists = array_flip(ParametersName::getAvailables());
/** @var ConfigurationParameterMutableInterface $parameter */
foreach ($this->map as $name => $parameter) {
if (! ParametersName::isExists($name)) {
$this->removed[$name] = $parameter;
} else {
$definition = $this->definitions->getDefinition($name);
$parameter
->setTitle($definition['title'])
->setSection($definition['section']);
$this->changed[$name] = $parameter;
unset($notExists[$name]);
}
}
$notExists = array_keys($notExists);
foreach ($notExists as $name) {
$this->map[$name] = $this->createParameter($name);
$this->changed[$name] = true;
}
$this->sync();
}
/**
* Create default parameter from config.
*
* @param string $name Parameter name.
*
* @return ConfigurationParameterInterface
*/
abstract protected function createParameter($name);
/**
* Load configuration from storage.
*
* @return ConfigurationParameterInterface[]
*/
abstract protected function loadData();
/**
* @param ConfigurationParameterInterface[]|array $changed Array of changed
* instances.
* @param ConfigurationParameterInterface[]|array $removed Array of removed
* parameter names.
*
* @return void
*/
abstract protected function doSync(array $changed, array $removed);
}
@@ -0,0 +1,324 @@
<?php
namespace AppBundle\Configuration;
use Ivory\CKEditorBundle\Form\Type\CKEditorType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Traversable;
/**
* Class ConfigurationDefinitionMap
* @package AppBundle\Configuration
*/
class ConfigurationDefinitionMap implements \IteratorAggregate
{
/**
* @var string[]
*/
private static $availablePeriods = [
'day',
'days',
'week',
'weeks',
'month',
'months',
'year',
'years',
'hour',
'hours',
'minute',
'minutes',
'second',
'seconds',
];
/**
* @var array[]
*/
private $definitions;
/**
* ConfigurationDefinitionMap constructor.
*/
public function __construct()
{
$this->definitions = [
ParametersName::MAILER_ADDRESS => [
'section' => 'Mailer',
'title' => 'support@socialhose.io',
'type' => 'string',
'formType' => null,
'default' => 'support@socialhose.io',
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'string' ]),
],
ParametersName::MAILER_SENDER_NAME => [
'section' => 'Mailer',
'title' => 'Socialhose',
'type' => 'string',
'formType' => null,
'default' => 'Socialhose',
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'string' ]),
],
ParametersName::NOTIFICATION_COMMENTS_PER_DOCUMENT => [
'section' => 'Notification',
'title' => 'Max comments per document',
'type' => 'integer',
'formType' => null,
'default' => 5,
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'numeric' ]),
],
ParametersName::NOTIFICATION_DOCUMENT_PER_FEED => [
'section' => 'Notification',
'title' => 'Max documents per feed in notification',
'type' => 'integer',
'formType' => null,
'default' => 10,
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'numeric' ]),
],
ParametersName::NOTIFICATION_START_EXTRACT_LENGTH => [
'section' => 'Search',
'title' => 'Number of character for \'Start of text extract\'',
'type' => 'integer',
'formType' => null,
'default' => 400,
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'numeric' ]),
],
ParametersName::NOTIFICATION_CONTEXT_EXTRACT_LENGTH => [
'section' => 'Search',
'title' => 'Numbers of character before and after first search keyword',
'type' => 'integer',
'formType' => null,
'default' => 150,
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'numeric' ]),
],
ParametersName::SEARCH_DOCUMENTS_FROM_FUTURE => [
'section' => 'Search',
'title' => 'What we should do if documents published date in future',
'type' => 'string',
'formType' => ChoiceType::class,
'choices' => [
'Exclude' => 'exclude',
'Fix date' => 'fix_date',
],
'default' => 'exclude',
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'numeric' ]),
],
ParametersName::NOTIFICATION_EMPTY_MESSAGE => [
'section' => 'Notification',
'title' => 'Empty notification message',
'type' => 'string',
'formType' => CKEditorType::class,
'default' => '<p>We have not found any mentions for your search criteria today.</p>',
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'string' ]),
],
ParametersName::NOTIFICATION_SEND_HISTORY_MODIFY => [
'section' => 'Notification',
'title' => 'How long we story notification history',
'type' => 'string',
'formType' => null,
'default' => '-3 months',
'normalizer' => function ($value) {
return preg_replace('/(\d+)/', '-$1', str_replace('-', '', $value));
},
'denormalizer' => function ($value) {
return str_replace('-', '', $value);
},
'constrains' => [
new Type([ 'type' => 'string' ]),
new Callback([ $this, 'validateHistoryLifetime' ]),
],
],
ParametersName::REGISTRATION_PAYMENT_AWAITING => [
'section' => 'Registration',
'title' => 'Message after user provide billing information',
'type' => 'string',
'formType' => CKEditorType::class,
'default' => '<p>Thanks for submitting the form. Your payment is processing. When it done, you will receive email with passwird.</p>',
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'string' ]),
],
ParametersName::MAIL_PASSWORD => [
'section' => 'Email',
'title' => 'Password email content',
'type' => 'string',
'formType' => CKEditorType::class,
'default' => '<h4>Hello {{ user.firstName }} {{ user.lastName }}!</h4><p>You new password is {{ password }}</p><p>Regards, the Team.</p>',
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'string' ]),
],
ParametersName::MAIL_VERIFICATION_SUCCESS => [
'section' => 'Email',
'title' => 'Verification success email content',
'type' => 'string',
'formType' => CKEditorType::class,
'default' => '<h4>Hello {{ user.firstName }} {{ user.lastName }}!</h4><p>You registration is verified and you may proceed login with you credentials </p><p> Email: {{ user.email }} Password: {{ password }}</p><p>Regards, the Team.</p>',
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'string' ]),
],
ParametersName::MAIL_VERIFICATION_REJECT => [
'section' => 'Email',
'title' => 'Verification success email content',
'type' => 'string',
'formType' => CKEditorType::class,
'default' => '<h4>Hello {{ user.firstName }} {{ user.lastName }}!</h4><p>Unfortunately you registration is rejected. Payments will be refund. </p><p>Regards, the Team.</p>',
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'string' ]),
],
ParametersName::MAIL_RESETTING_CONFIRMATION => [
'section' => 'Email',
'title' => 'Password resetting email content',
'type' => 'string',
'formType' => CKEditorType::class,
'default' => '<h4>Hello {{ user.firstName }} {{ user.lastName }}!</h4> <p> To reset your password - please visit {{ confirmationUrl }} </p> <p> Regards, the Team. </p>',
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'string' ]),
],
ParametersName::MAIL_UNSUBSCRIBE => [
'section' => 'Email',
'title' => 'Unsubscribe email content',
'type' => 'string',
'formType' => CKEditorType::class,
'default' => '<p>{{ user.firstName}} {{ user.lastName }} has unsubscribed from your notification</p>',
'normalizer' => null,
'denormalizer' => null,
'constrains' => new Type([ 'type' => 'string' ]),
],
];
}
/**
* Get definition for specified parameter.
*
* @param string $name Parameter name.
*
* @return array
*/
public function getDefinition($name)
{
if (! isset($this->definitions[$name])) {
throw new \InvalidArgumentException('Unknown '. $name);
}
return $this->definitions[$name];
}
/**
* Normalize value.
*
* @param string $name Parameter name.
* @param mixed $value Raw value.
*
* @return mixed
*/
public function normalize($name, $value)
{
$definition = $this->getDefinition($name);
if (isset($definition['normalizer'])) {
$value = $definition['normalizer']($value);
}
return $value;
}
/**
* Denormalize value.
*
* @param string $name Parameter name.
* @param mixed $value Normalized value.
*
* @return mixed
*/
public function denormalize($name, $value)
{
$definition = $this->getDefinition($name);
if (isset($definition['denormalizer'])) {
$value = $definition['denormalizer']($value);
}
return $value;
}
/**
* Retrieve an external iterator.
*
* @return Traversable An instance of an object implementing \Iterator or
* \Traversable
*/
public function getIterator()
{
return new \ArrayIterator($this->definitions);
}
/**
* @param string $value Raw value from user.
* @param ExecutionContextInterface $context A ExecutionContextInterface instance.
*
* @return void
*/
public function validateHistoryLifetime($value, ExecutionContextInterface $context)
{
//
// Split expiration time on groups
//
$valid = true;
$matches = [];
$result = preg_match_all('/(\d+\s?[A-Za-z]+)/', $value, $matches);
if (($result === 0) || ($result === false)) {
$valid = false;
} else {
$matches = $matches[0];
foreach ($matches as $match) {
$match = trim($match);
$parts = explode(' ', $match);
$count = 1;
$period = $parts[0];
if (count($parts) === 2) {
list($count, $period) = $parts;
}
if (! is_numeric($count) || !in_array($period, self::$availablePeriods, true)) {
$valid = false;
break;
}
}
}
if (! $valid) {
$context->buildViolation('Invalid expiration time, should be space separated string where each element id match to patter "number year(s)|month(s)|week(s)|day(s)|hour(s)|minute(s)|second(s)"')
->atPath('expirationTime')
->addViolation();
}
}
}
@@ -0,0 +1,28 @@
<?php
namespace AppBundle\Configuration;
/**
* Interface ConfigurationImmutableInterface
* @package AppBundle\Configuration
*/
interface ConfigurationImmutableInterface
{
/**
* Get parameter value by name.
*
* @param string $name Parameter name.
* @param mixed $default Default value if parameter not found.
*
* @return mixed
*/
public function getParameter($name, $default = null);
/**
* Sync current parameters with database.
*
* @return void
*/
public function syncParameters();
}
@@ -0,0 +1,13 @@
<?php
namespace AppBundle\Configuration;
/**
* Interface ConfigurationMutableInterface
* @package AppBundle\Configuration
*/
interface ConfigurationInterface extends
ConfigurationMutableInterface,
ConfigurationImmutableInterface
{
}
@@ -0,0 +1,52 @@
<?php
namespace AppBundle\Configuration;
/**
* Interface ConfigurationMutableInterface
* @package AppBundle\Configuration
*/
interface ConfigurationMutableInterface
{
/**
* Get all available parameters.
*
* @return ConfigurationParameterInterface[]
*/
public function getParameters();
/**
* Sync parameters with list of available.
*
* @return void
*/
public function syncWithDefinitions();
/**
* Set parameter value by name.
*
* @param string $name Parameter name.
* @param mixed $value New parameter value.
*
* @return void
*/
public function setParameter($name, $value);
/**
* Set parameters.
*
* @param array $params Array where key is parameter name and value is new
* value.
*
* @return void
*/
public function setParameters(array $params);
/**
* Sync configuration with storage.
*
* @return void
*/
public function sync();
}
@@ -0,0 +1,48 @@
<?php
namespace AppBundle\Configuration;
/**
* Interface ConfigurationParameterInterface
* @package AppBundle\Configuration
*/
interface ConfigurationParameterInterface
{
/**
* Get parameter section.
*
* @return string
*/
public function getSection();
/**
* Get parameter name.
*
* @return string
*/
public function getName();
/**
* Get parameter title.
*
* @return string
*/
public function getTitle();
/**
* Get parameter value.
*
* @param mixed $value Parameter value.
*
* @return ConfigurationParameterInterface
*/
public function setValue($value);
/**
* Get parameter value.
*
* @return mixed
*/
public function getValue();
}
@@ -0,0 +1,38 @@
<?php
namespace AppBundle\Configuration;
/**
* Interface ConfigurationParameterMutableInterface
* @package AppBundle\Configuration
*/
interface ConfigurationParameterMutableInterface
{
/**
* Set section
*
* @param string $section Section name.
*
* @return ConfigurationParameterMutableInterface
*/
public function setSection($section);
/**
* Set value
*
* @param mixed $value Parameter value.
*
* @return ConfigurationParameterMutableInterface
*/
public function setValue($value);
/**
* Set title
*
* @param string $title Human readable parameter title.
*
* @return ConfigurationParameterMutableInterface
*/
public function setTitle($title);
}
@@ -0,0 +1,85 @@
<?php
namespace AppBundle\Configuration;
use AdminBundle\Entity\SiteSettings;
use Doctrine\ORM\EntityManagerInterface;
/**
* Class ORMConfiguration
* @package AppBundle\Configuration
*/
class ORMConfiguration extends AbstractConfiguration
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* Configuration constructor.
*
* @param ConfigurationDefinitionMap $definitions A ConfigurationDefinitionMap
* instance.
* @param EntityManagerInterface $em A EntityManagerInterface
* instance.
*/
public function __construct(
ConfigurationDefinitionMap $definitions,
EntityManagerInterface $em
) {
$this->em = $em;
parent::__construct($definitions);
}
/**
* Create default parameter from config.
*
* @param string $name Parameter name.
*
* @return ConfigurationParameterInterface
*/
protected function createParameter($name)
{
$config = $this->definitions->getDefinition($name);
return SiteSettings::create()
->setSection($config['section'])
->setName($name)
->setTitle($config['title'])
->setValue($config['default']);
}
/**
* Load configuration from storage.
*
* @return ConfigurationParameterInterface[]
*/
protected function loadData()
{
return $this->em->getRepository(SiteSettings::class)->findAll();
}
/**
* @param ConfigurationParameterInterface[]|array $changed Array of changed
* instances.
* @param ConfigurationParameterInterface[]|array $removed Array of removed
* parameter names.
*
* @return void
*/
protected function doSync(array $changed, array $removed)
{
foreach ($changed as $parameter) {
$this->em->persist($parameter);
}
foreach ($removed as $parameter) {
$this->em->remove($parameter);
}
$this->em->flush();
}
}
@@ -0,0 +1,85 @@
<?php
namespace AppBundle\Configuration;
/**
* Class ParametersName
* @package AppBundle\Configuration
*/
final class ParametersName
{
const MAILER_ADDRESS = 'mailer.address';
const MAILER_SENDER_NAME = 'mailer.sender_name';
const NOTIFICATION_COMMENTS_PER_DOCUMENT = 'notification.comments_per_document_limit';
const NOTIFICATION_DOCUMENT_PER_FEED = 'notification.documents_per_feed_limit';
const NOTIFICATION_START_EXTRACT_LENGTH = 'notification.start_extract_length';
const NOTIFICATION_CONTEXT_EXTRACT_LENGTH = 'notification.context_extract_length';
const NOTIFICATION_SEND_HISTORY_MODIFY = 'notification.notification_send_history_modify';
const NOTIFICATION_EMPTY_MESSAGE = 'notification.empty_massage';
const REGISTRATION_PAYMENT_AWAITING = 'registration.payment.awaiting';
const SEARCH_DOCUMENTS_FROM_FUTURE = 'search.documents_from_future';
const MAIL_PASSWORD = 'mail.password';
const MAIL_VERIFICATION_SUCCESS = 'mail.verification.success';
const MAIL_VERIFICATION_REJECT = 'mail.verification.reject';
const MAIL_RESETTING_CONFIRMATION = 'mail.resetting_confirmation';
const MAIL_UNSUBSCRIBE = 'mail.unsubscribe';
/**
* Get available parameters name.
*
* @return array
*/
public static function getAvailables()
{
return [
self::MAILER_ADDRESS,
self::MAILER_SENDER_NAME,
self::NOTIFICATION_COMMENTS_PER_DOCUMENT,
self::NOTIFICATION_DOCUMENT_PER_FEED,
self::NOTIFICATION_START_EXTRACT_LENGTH,
self::NOTIFICATION_CONTEXT_EXTRACT_LENGTH,
self::NOTIFICATION_SEND_HISTORY_MODIFY,
self::NOTIFICATION_EMPTY_MESSAGE,
self::REGISTRATION_PAYMENT_AWAITING,
self::SEARCH_DOCUMENTS_FROM_FUTURE,
self::MAIL_PASSWORD,
self::MAIL_VERIFICATION_SUCCESS,
self::MAIL_VERIFICATION_REJECT,
self::MAIL_RESETTING_CONFIRMATION,
self::MAIL_UNSUBSCRIBE,
];
}
/**
* @param string $name Checks that specified parameter is exists.
*
* @return boolean
*/
public static function isExists($name)
{
return in_array($name, self::getAvailables(), true);
}
/**
* Parameters constructor.
*/
private function __construct()
{
}
/**
* @return void
*/
private function __clone()
{
}
}
@@ -0,0 +1,181 @@
<?php
namespace AppBundle\Controller;
use AppBundle\Controller\V1\AbstractV1Controller;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Feed\Formatter\FeedFormatterInterface;
use CacheBundle\Feed\Formatter\FormatterOptions;
use CacheBundle\Repository\CommonFeedRepository;
use Common\Enum\FormatNameEnum;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use UserBundle\Enum\ThemeOptionExtractEnum;
/**
* Class IndexController
* @package AppBundle\Controller
*
* @Route("/", service="app.controller.index")
*/
class IndexController extends AbstractV1Controller
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var FeedFormatterInterface
*/
private $feedFormatter;
/**
* IndexController constructor.
*
* @param EntityManagerInterface $em A EntityManagerInterface
* instance.
* @param FeedFormatterInterface $feedFormatter A FeedFormatterInterface
* instance.
*/
public function __construct(EntityManagerInterface $em, FeedFormatterInterface $feedFormatter)
{
$this->em = $em;
$this->feedFormatter = $feedFormatter;
}
/**
* @Route("/feed/{id}.{format}")
*
* @param Request $request A HTTP Request instance.
* @param integer $id A Feed entity id.
* @param string $format A format name.
*
* @return Response
*/
public function exportFeedAction(Request $request, $id, $format)
{
/** @var CommonFeedRepository $repository */
$repository = $this->em->getRepository(AbstractFeed::class);
$feed = $repository->find($id);
if ((! $feed instanceof AbstractFeed) || ! $feed->getExported()) {
throw $this->createNotFoundException();
}
$format = strtolower(trim($format));
if (! FormatNameEnum::isValid($format)) {
throw new BadRequestHttpException('Unknown format '. $format);
}
$data = $this->feedFormatter->formatFeed($feed, new FormatterOptions(
new FormatNameEnum($format),
$this->getNumber($request),
$this->getExtract($request),
$this->getShowImage($request),
$this->getAsPlain($request)
));
return Response::create($data->getData(), 200, [
'Content-Type' => $data->getMime(),
]);
}
/**
* @Route(
* "/{part}",
* methods={ "GET" },
* requirements={ "part"=".*" },
* defaults={ "part"="" }
* )
* @Template("AppBundle::index.html.twig")
*
* @return array
*/
public function indexAction()
{
return [];
}
/**
* @param Request $request A HTTP Request instance.
*
* @return integer
*/
private function getNumber(Request $request)
{
$number = $request->query->getInt('n', 30);
if (($number < 1) || ($number > 200)) {
throw new BadRequestHttpException("'n' should be integer between 1 and 200.");
}
return $number;
}
/**
* @param Request $request A HTTP Request instance.
*
* @return ThemeOptionExtractEnum
*/
private function getExtract(Request $request)
{
$extract = strtolower(trim($request->query->get('ext', 'n')));
switch ($extract) {
case 's':
$extract = ThemeOptionExtractEnum::start();
break;
case 'sc':
$extract = ThemeOptionExtractEnum::context();
break;
case 'n':
$extract = ThemeOptionExtractEnum::no();
break;
default:
throw new BadRequestHttpException("'ext' should be one of: s, sc, n.");
}
return $extract;
}
/**
* @param Request $request A Request instance.
*
* @return boolean
*/
private function getShowImage(Request $request)
{
$showImage = $request->query->get('img', '0');
if (($showImage !== '0') && ($showImage !== '1')) {
throw new BadRequestHttpException("'img' should be 0 or 1.");
}
return $showImage === '1';
}
/**
* @param Request $request A Request instance.
*
* @return boolean
*/
private function getAsPlain(Request $request)
{
$textFormat = strtolower(trim($request->query->get('text_format')));
if (($textFormat !== '') && ($textFormat !== 'text')) {
throw new BadRequestHttpException("'text_format' should not be defined or contains 'text' value.");
}
return $textFormat === 'text';
}
}
@@ -0,0 +1,43 @@
<?php
namespace AppBundle\Controller\Traits;
use ApiBundle\Security\AccessChecker\AccessCheckerInterface;
/**
* Trait AccessCheckerTrait
*
* @package AppBundle\Controller\Traits
*/
trait AccessCheckerTrait
{
/**
* @var AccessCheckerInterface
*/
protected $accessChecker;
/**
* @param string $action Action name.
* @param object|object[] $entity A Entity instance or array of entity instances.
*
* @return string[] Array of restriction reasons.
*/
protected function checkAccess($action, $entity)
{
if ($entity instanceof \Traversable) {
$entity = iterator_to_array($entity);
} elseif (is_object($entity)) {
$entity = [ $entity ];
}
if (! is_array($entity)) {
throw new \InvalidArgumentException('Expects single object or array of objects.');
}
$grantChecker = \nspl\f\partial([ $this->accessChecker, 'isGranted' ], $action);
return \nspl\a\flatten(\nspl\a\map($grantChecker, $entity));
}
}
@@ -0,0 +1,50 @@
<?php
namespace AppBundle\Controller\Traits;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
/**
* Trait FormFactoryAwareTrait
*
* @package AppBundle\Controller\Traits
*/
trait FormFactoryAwareTrait
{
/**
* @var FormFactoryInterface
*/
protected $formFactory;
/**
* Creates and returns a Form instance from the type of the form.
*
* @param string $type The fully qualified class name of the form type.
* @param mixed $data The initial data for the form.
* @param array $options Options for the form.
*
* @return FormInterface
*/
protected function createForm($type, $data = null, array $options = array())
{
return $this->formFactory->create($type, $data, $options);
}
/**
* Creates and returns a form builder instance.
*
* @param mixed $data The initial data for the form.
* @param array $options Options for the form.
*
* @return FormBuilderInterface
*/
protected function createFormBuilder($data = null, array $options = array())
{
return $this->formFactory->createBuilder(FormType::class, $data, $options);
}
}
@@ -0,0 +1,39 @@
<?php
namespace AppBundle\Controller\Traits;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use UserBundle\Entity\User;
/**
* Trait TokenStorageAwareTrait
*
* @package AppBundle\Controller\Traits
* @deprecated
*/
trait TokenStorageAwareTrait
{
/**
* @var TokenStorageInterface
*/
protected $tokenStorage;
/**
* Return current use if it exists.
*
* @return User|null
*/
public function getCurrentUser()
{
$user = null;
$token = $this->tokenStorage->getToken();
if ($token !== null) {
$user = $token->getUser();
}
return $user;
}
}
@@ -0,0 +1,97 @@
<?php
namespace AppBundle\Controller\V1;
use ApiBundle\Controller\AbstractApiController;
use ApiBundle\Response\View;
use AppBundle\Response\SearchResponseInterface;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class AbstractV1Controller
*
* @package AppBundle\Controller\V1
*
* @deprecated
* @see AbstractApiController
*/
abstract class AbstractV1Controller
{
/**
* Generate proper server response.
*
* @param mixed $data A data sent to client.
* @param integer $code Response http code.
* @param array $groups Serialization groups.
*
* @return \ApiBundle\Response\ViewInterface
*/
protected function generateResponse(
$data = null,
$code = null,
array $groups = []
) {
return new View($data, $groups, $code);
}
/**
* @param mixed $data A data for pagination.
* @param integer $page A requested page, starts from 1.
* @param integer $limit Required numbers of data per page.
*
* @return array
*/
protected function paginate($data, $page, $limit)
{
if ($data instanceof SearchResponseInterface) {
//
// Response from index or cache already paginated so we just return
// values.
//
return [
'data' => $data->getDocuments(),
'count' => count($data),
'totalCount' => $data->getTotalCount(),
'page' => $page,
'limit' => $limit,
];
} elseif ($data instanceof QueryBuilder) {
$data
->setMaxResults($limit)
->setFirstResult(($page - 1) * $limit);
$paginator = new Paginator($data);
$data = iterator_to_array($paginator);
return [
'data' => $data,
'count' => count($data),
'totalCount' => $paginator->count(),
'page' => $page,
'limit' => $limit,
];
}
return [];// TODO add code for over paginated data.
}
/**
* Returns a NotFoundHttpException.
*
* This will result in a 404 response code. Usage example:
*
* throw $this->createNotFoundException('Page not found!');
*
* @param string $message A message.
* @param \Exception|null $previous The previous exception.
*
* @return NotFoundHttpException
*/
protected function createNotFoundException($message = 'Not Found', \Exception $previous = null)
{
return new NotFoundHttpException($message, $previous);
}
}
@@ -0,0 +1,286 @@
<?php
namespace AppBundle\Controller\V1;
use ApiBundle\Controller\AbstractApiController;
use ApiBundle\Entity\ManageableEntityInterface;
use ApiBundle\Form\EntitiesBatchType;
use ApiBundle\Security\AccessChecker\AccessCheckerInterface;
use ApiBundle\Security\Inspector\InspectorInterface;
use AppBundle\Controller\Traits\AccessCheckerTrait;
use AppBundle\Controller\Traits\FormFactoryAwareTrait;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Class AbstractV1CrudController
*
* @package AppBundle\Controller\V1
*
* @deprecated
* @see AbstractApiController
*/
abstract class AbstractV1CrudController extends AbstractV1Controller
{
use
FormFactoryAwareTrait,
AccessCheckerTrait;
/**
* @var EntityManagerInterface
*/
protected $em;
/**
* Entity fqcn.
*
* @var string
*/
protected $entity;
/**
* AbstractV1CrudController constructor.
*
* @param FormFactoryInterface $formFactory A FormFactoryInterface instance.
* @param AccessCheckerInterface $accessChecker A AccessCheckerInterface instance.
* @param EntityManagerInterface $em A EntityManagerInterface instance.
* @param string $entity A used entity name.
*/
public function __construct(
FormFactoryInterface $formFactory,
AccessCheckerInterface $accessChecker,
EntityManagerInterface $em,
$entity
) {
$this->formFactory = $formFactory;
$this->accessChecker = $accessChecker;
$this->em = $em;
$this->entity = $entity;
}
/**
* Create new entity.
*
* @param Request $request A Request instance.
* @param ManageableEntityInterface $entity A ManageableEntityInterface
* instance.
*
* @return \ApiBundle\Entity\ManageableEntityInterface|\ApiBundle\Response\ViewInterface
*/
protected function createEntity(Request $request, ManageableEntityInterface $entity)
{
$form = $this->createForm($entity->getCreateFormClass(), $entity);
// Submit data into form.
$form->submit($request->request->all());
if ($form->isValid()) {
// Check that current user can create this entity.
// If user don't have rights to create this entity we should send all
// founded restrictions to client.
$reasons = $this->checkAccess(InspectorInterface::CREATE, $entity);
if (count($reasons) > 0) {
// User don't have rights to create this entity so send all
// founded restriction reasons to client.
return $this->generateResponse($reasons, 403);
}
$this->em->persist($entity);
$this->em->flush();
return $entity;
}
// Client send invalid data.
return $this->generateResponse($form, 400);
}
/**
* Get information about single entity.
*
* @param integer|ManageableEntityInterface|null $id A entity id.
*
* @return \ApiBundle\Entity\ManageableEntityInterface|\ApiBundle\Response\ViewInterface
*/
protected function getEntity($id)
{
$foundedEntity = $id;
if (is_numeric($id)) {
$repository = $this->em->getRepository($this->entity);
$foundedEntity = $repository->find($id);
}
if ($foundedEntity === null) {
$name = \app\c\getShortName($this->entity);
// Remove 'Abstract' prefix if it exists.
if (strpos($name, 'Abstract') !== false) {
$name = substr($name, 8);
}
return $this->generateResponse("Can't find {$name} with id {$id}.", 404);
}
// Check that current user can read this entity.
// If user don't have rights to read this entity we should send all
// founded restrictions to client.
$reasons = $this->checkAccess(InspectorInterface::READ, $foundedEntity);
if (count($reasons) > 0) {
return $this->generateResponse($reasons, 403);
}
return $foundedEntity;
}
/**
* Update entity.
*
* @param Request $request A Request instance.
* @param integer|ManageableEntityInterface|null $entity A entity id.
*
* @return \ApiBundle\Entity\ManageableEntityInterface|\ApiBundle\Response\ViewInterface
*/
protected function putEntity(Request $request, $entity)
{
$foundedEntity = $entity;
if (is_numeric($entity)) {
$repository = $this->em->getRepository($this->entity);
/** @var \ApiBundle\Entity\ManageableEntityInterface $entity */
$foundedEntity = $repository->find($entity);
}
if ($foundedEntity === null) {
$name = \app\c\getShortName($this->entity);
// Remove 'Abstract' prefix if it exists.
if (strpos($name, 'Abstract') !== false) {
$name = substr($name, 8);
}
return $this->generateResponse("Can't find {$name} with id {$entity}.", 404);
}
$form = $this->createForm($foundedEntity->getUpdateFormClass(), $foundedEntity, [
'method' => 'PUT',
]);
$form->submit($request->request->all());
if ($form->isValid()) {
// Check that current user can update this entity.
// If user don't have rights to update this entity we should send all
// founded restrictions to client.
$reasons = $this->checkAccess(InspectorInterface::UPDATE, $foundedEntity);
if (count($reasons) > 0) {
return $this->generateResponse($reasons, 403);
}
$this->em->persist($foundedEntity);
$this->em->flush();
return $foundedEntity;
}
return $this->generateResponse($form, 400);
}
/**
* Delete entity.
*
* @param integer|ManageableEntityInterface|null $entity A entity id.
*
* @return \ApiBundle\Response\ViewInterface
*/
protected function deleteEntity($entity)
{
$foundedEntity = $entity;
if (is_numeric($entity)) {
$repository = $this->em->getRepository($this->entity);
/** @var \ApiBundle\Entity\ManageableEntityInterface $entity */
$foundedEntity = $repository->find($entity);
}
if ($foundedEntity === null) {
$name = \app\c\getShortName($this->entity);
// Remove 'Abstract' prefix if it exists.
if (strpos($name, 'Abstract') !== false) {
$name = substr($name, 8);
}
return $this->generateResponse("Can't find {$name} with id {$entity}.", 404);
}
// Check that current user can delete this entity.
// If user don't have rights to delete this entity we should send all
// founded restrictions to client.
$reasons = $this->checkAccess(InspectorInterface::DELETE, $foundedEntity);
if (count($reasons) > 0) {
return $this->generateResponse($reasons, 403);
}
$this->em->remove($foundedEntity);
$this->em->flush();
return $this->generateResponse();
}
/**
* @param Request $request A Request instance.
* @param string|callable $permission A requested permission.
* @param string $formClass Form class fqcn.
* @param callable $processor Function which process founded entities.
*
* @return \ApiBundle\Response\ViewInterface
*/
protected function batchProcessing(
Request $request,
$permission,
$formClass,
callable $processor
) {
$this->checkFormClass($formClass);
$form = $this->createForm($formClass, null, [ 'class' => $this->entity ]);
$form->submit($request->request->all());
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
if (is_callable($permission)) {
$permission = call_user_func_array($permission, $data);
}
if (! is_string($permission)) {
throw new \InvalidArgumentException('$permission should be string or callable');
}
$reasons = $this->checkAccess($permission, $data['entities']);
if (count($reasons) > 0) {
return $this->generateResponse($reasons, 403);
}
$response = call_user_func_array($processor, $data);
if ($response === null) {
$response = $this->generateResponse();
}
return $response;
}
return $this->generateResponse($form, 400);
}
/**
* @param string $formClass Form class fqcn.
*
* @return void
*/
private function checkFormClass($formClass)
{
if (! is_string($formClass) || ! class_exists($formClass)) {
throw new \InvalidArgumentException('$formClass should be fqcn');
}
if (($formClass !== EntitiesBatchType::class)
&& ! in_array(EntitiesBatchType::class, class_parents($formClass), true)) {
throw new \InvalidArgumentException('Invalid form class '. $formClass);
}
}
}
@@ -0,0 +1,288 @@
<?php
namespace AppBundle\Controller\V1;
use ApiBundle\Controller\AbstractCRUDController;
use ApiBundle\Controller\Annotation\Roles;
use ApiDocBundle\Controller\Annotation\AppApiDoc;
use AppBundle\Exception\NotAllowedException;
use CacheBundle\DTO\AnalyticDTO;
use CacheBundle\Entity\Analytic\Analytic;
use CacheBundle\Form\AnalyticType;
use CacheBundle\Service\Factory\Analytic\AnalyticFactoryInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use ApiBundle\Security\Inspector\InspectorInterface;
use CacheBundle\Repository\AnalyticRepository;
/**
* Class AnalyticController
* @package AppBundle\Controller\V1
*
* @Route("/analysis", service="app.controller.analytic")
*/
class AnalyticController extends AbstractCRUDController
{
/**
* @var AnalyticFactoryInterface
*/
private $analyticFactory;
/**
* Create new analytic entity.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* methods={ "POST" }
* )
* @AppApiDoc(
* section="Analytic",
* resource=false,
* input={
* "class"="CacheBundle\Form\AnalyticType"
* },
* output={
* "class"="CacheBundle\Entity\Analytic\Analytic",
* "groups"={ "analytic", "id" }
* },
* statusCodes={
* 200="Analytics successfully created.",
* 400="Invalid data provided.",
* 403="You don't have permissions to create analytics."
* }
* )
*
* @param Request $request A Http Request instance.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function postAction(Request $request)
{
$user = $this->getCurrentUser();
$this->analyticFactory = $this->get('cache.analytic_factory');
$form = $this->createForm(AnalyticType::class)
->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var AnalyticDTO $dto */
$dto = $form->getData();
try {
$analytic = $this->analyticFactory->createAnalytic($dto, $user);
} catch (NotAllowedException $exception) {
return $this->generateResponse('You not allowed to make analytics');
}
$this->getManager()->persist($analytic);
$this->getManager()->flush();
return $this->generateResponse($analytic, 200, ['id', 'analytic']);
}
return $this->generateResponse($form, 400);
}
/**
* Get specified analytic by id.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* "/{id}",
* requirements={ "id"="\d+" },
* methods={ "GET" }
* )
* @AppApiDoc(
* resource=true,
* section="Analytic",
* output={
* "class"="CacheBundle\Entity\Analytic\Analytic",
* "groups"={"id"}
* },
* statusCodes={
* 200="Analytics successfully returned.",
* 403="You don't have permissions to view this analytics.",
* 404="Can't find analytic by specified id."
* }
* )
*
* @param integer $id Analytic entity id.
*
* @return \CacheBundle\Entity\Analytic\Analytic|\ApiBundle\Response\ViewInterface
*/
public function getAction($id)
{
return parent::getEntity($id);
}
/**
* Delete specified analytic.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* "/{id}",
* requirements={ "id"="\d+" },
* methods={ "DELETE" }
* )
* @AppApiDoc(
* resource=true,
* section="Analytic",
* statusCodes={
* 204="Analytic successfully deleted.",
* 403="You don't have permissions to delete this analytic.",
* 404="Can't find analytic by specified id."
* }
* )
*
* @param integer $id A Analytic entity id.
*
* @return array|\ApiBundle\Response\ViewInterface
*/
public function deleteAction($id)
{
$repository = $this->getManager()->getRepository($this->entity);
/** @var Analytic $analytic */
$analytic = $repository->find($id);
if (!$analytic instanceof Analytic) {
return $this->generateResponse("Can't find analytic with id {$id}.", 404);
}
$reasons = $this->checkAccess(InspectorInterface::DELETE, $analytic);
if (count($reasons) > 0) {
return $this->generateResponse($reasons, 403);
}
$analyticContext = $analytic->getContext();
if (isset($analyticContext)) {
($analyticContext->getAnalytics()->count() == 1) ? $this->getManager()->remove($analyticContext) : "";
}
$this->getManager()->remove($analytic);
$this->getManager()->flush();
return $this->generateResponse();
}
/**
* Update analytic.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route("/{id}", methods={ "PUT" }, requirements={ "id"="\d+" })
* @AppApiDoc(
* section="Analytic",
* resource=true,
* input={
* "class"="CacheBundle\Form\AnalyticType",
* "name"=false
* },
* output={
* "class"="CacheBundle\Entity\Analytic\Analytic",
* "groups"={ "analytic", "id" }
* },
* statusCodes={
* 200="Analytics successfully updated.",
* 400="Invalid data provided."
* }
* )
*
* @param Request $request A Request instance.
* @param integer $id Analytic entity id.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function putAction(Request $request, $id)
{
$user = $this->getCurrentUser();
$this->analyticFactory = $this->get('cache.analytic_factory');
/** @var AnalyticRepository $analyticRepository */
$repository = $this->getManager()->getRepository($this->entity);
/** @var Analytic $analytic */
$analytic = $repository->find($id);
if (!$analytic instanceof Analytic) {
return $this->generateResponse("Can't find analytic with id {$id}.", 404);
}
$feeds = $analytic->getContext()->getFeeds();
$feedsId = [];
foreach ($feeds as $feedsVal) {
$feedsId[] = $feedsVal->getId();
}
$analyticDto = new AnalyticDTO($feedsId, null, $analytic->getContext()->getFilters(), $analytic->getContext()->getRawFilters());
$form = $this->createForm(AnalyticType::class, $analyticDto);
$form->submit($request->request->all());
if ($form->isValid()) {
/** @var AnalyticDTO $dto */
$dto = $form->getData();
try {
$analyticContext = $analytic->getContext();
if (isset($analyticContext)) {
($analyticContext->getAnalytics()->count() == 1) ? $this->getManager()->remove($analyticContext) : "";
}
$analytic = $this->analyticFactory->updateAnalytic($dto, $user, $analytic);
} catch (NotAllowedException $exception) {
return $this->generateResponse('You not allowed to update analytics');
}
$this->getManager()->persist($analytic);
$this->getManager()->flush();
return $this->generateResponse($analytic, 200, ['id', 'analytic']);
}
return $this->generateResponse($form, 400);
}
/**
* Get list of categories for current user.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(methods={ "GET" })
* @AppApiDoc(
* section="Analytic",
* output={
* "class"="Pagination<CacheBundle\Entity\Analytic\Analytic>",
* "groups"={ "analytic", "id","context" }
* },
* statusCodes={
* 200="List of analytic successfully returned."
* }
* )
*
* @param Request $request
* @return array|\ApiBundle\Response\ViewInterface
*/
public function listAction(Request $request)
{
/** @var AnalyticRepository $repository */
$repository = $this->getManager()->getRepository(Analytic::class);
$user = $this->getCurrentUser();
$pagination = $this->paginate(
$request,
$repository->getList($user->getId())
);
// Simulate pagination serialization.
return $this->generateResponse([
$pagination
], 200, [
'analytic',
'id',
'context'
]);
}
}
@@ -0,0 +1,250 @@
<?php
namespace AppBundle\Controller\V1;
use ApiBundle\Controller\AbstractCRUDController;
use ApiBundle\Controller\Annotation\Roles;
use CacheBundle\Entity\Analytic\Analytic;
use DateInterval;
use DatePeriod;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use CacheBundle\Repository\AnalyticRepository;
use CacheBundle\Entity\Document;
use Symfony\Component\HttpFoundation\Request;
/**
* Class AnalyticGraphController
* @package AppBundle\Controller\V1
*
* @Route(service="app.controller.analytic-graph")
*/
class AnalyticGraphController extends AbstractCRUDController
{
/**
* Get data for influence list
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* "/influencer/{id}",
* requirements={
* "id": "\d+",
* },
* methods={ "POST" }
* )
*
* @param Request $request
* @param $id
*
* @return array|\ApiBundle\Response\ViewInterface
*
*/
public function getInfluenceAction(Request $request, $id)
{
$isAuthorType = $request->request->get('isAuthorType', false);
$groupByField = 'source_hashcode';
if ($isAuthorType === true) {
$groupByField = 'author_name';
}
/** @var AnalyticRepository $analyticRepository */
$repository = $this->getManager()->getRepository($this->entity);
/** @var Analytic $analytic */
$analytic = $repository->find($id);
if (!$analytic instanceof Analytic) {
return $this->generateResponse("Can't find analytic with id {$id}.", 404);
}
$analyticContext = $analytic->getContext();
$feeds = $analyticContext->getFeeds();
$queryId = [];
$clipFeedId = [];
foreach ($feeds as $feedsVal) {
if ($feedsVal->getSubType() == 'query_feed') {
$queryId[] = $feedsVal->getQuery()->getId();
} else {
$clipFeedId[] = $feedsVal->getId();
}
}
$filters = $analyticContext->getFilters();
$influenceData = [];
if (count($filters) > 0) {
if (array_key_exists('date', $filters)) {
$startDt = $filters['date']->getFilters()[0]->getValue()->format('Y-m-d');
$endDt = $filters['date']->getFilters()[1]->getValue()->format('Y-m-d');
$repository = $this->getManager()->getRepository(Document::class);
$documents = $repository->getByQuery($queryId);
$clipDocuments = $repository->getByClip($clipFeedId);
foreach ($feeds as $key => $feedsVal) {
$influenceData[$key]['name'] = $feedsVal->getName();
$influenceData[$key]['data'] = [];
foreach ($documents as $document) {
if ($feedsVal->getSubType() == 'query_feed') {
if ($feedsVal->getQuery()->getId() == $document['id']) {
$publishDate = substr($document['data']['published'], 0, 10);
$publishDate = date('Y-m-d', strtotime($publishDate));
if (($publishDate >= $startDt) && ($publishDate <= $endDt)) {
if (array_key_exists($groupByField, $document['data'])) {
$engagementCount = 0;
if (array_key_exists("likes", $document['data'])) {
$engagementCount = $document['data']['likes'];
}
if (array_key_exists("dislikes", $document['data'])) {
$engagementCount += $document['data']['dislikes'];
}
if (array_key_exists("comments", $document['data'])) {
$engagementCount += $document['data']['comments'];
}
if (array_key_exists("shares", $document['data'])) {
$engagementCount += $document['data']['shares'];
}
$tempInfluenceData = $influenceData[$key]['data'];
if (count($tempInfluenceData) > 0) {
$sourceHashCodeKey = array_search($document['data'][$groupByField], array_column($tempInfluenceData, $groupByField));
if ($sourceHashCodeKey === false) {
$tempData = [$groupByField => $document['data'][$groupByField], 'influence' => $document['data']['source_link'],
'source_type' => $document['data']['source_publisher_type'], 'engagement' => $engagementCount, 'totalSentiment' => 0];
if (array_key_exists("sentiment", $document['data'])) {
$sentiment = 1;
$tempData['totalSentiment'] = $sentiment;
$tempData[$document['data']['sentiment']] = $sentiment;
}
array_push($tempInfluenceData, $tempData);
$influenceData[$key]['data'] = $tempInfluenceData;
} else {
if (array_key_exists("sentiment", $document['data'])) {
$influenceData[$key]['data'][$sourceHashCodeKey]['totalSentiment'] += 1;
if (array_key_exists($document['data']['sentiment'], $influenceData[$key]['data'][$sourceHashCodeKey])) {
$influenceData[$key]['data'][$sourceHashCodeKey][$document['data']['sentiment']] += 1;
} else {
$influenceData[$key]['data'][$sourceHashCodeKey][$document['data']['sentiment']] = 1;
}
}
$influenceData[$key]['data'][$sourceHashCodeKey]['engagement'] += $engagementCount;
}
} else {
$tempData = [$groupByField => $document['data'][$groupByField], 'influence' => $document['data']['source_link'],
'source_type' => $document['data']['source_publisher_type'], 'engagement' => $engagementCount, 'totalSentiment' => 0];
if (array_key_exists("sentiment", $document['data'])) {
$sentiment = 1;
$tempData['totalSentiment'] = $sentiment;
$tempData[$document['data']['sentiment']] = $sentiment;
}
$influenceData[$key]['data'][0] = $tempData;
}
}
}
}
}
}
foreach ($clipDocuments as $clipDocument) {
if ($feedsVal->getSubType() == 'clip_feed') {
if ($feedsVal->getId() == $clipDocument['clipFeedId']) {
$publishDate = substr($clipDocument['data']['published'], 0, 10);
$publishDate = date('Y-m-d', strtotime($publishDate));
if (($publishDate >= $startDt) && ($publishDate <= $endDt)) {
if (array_key_exists($groupByField, $clipDocument['data'])) {
$engagementCount = 0;
if (array_key_exists("likes", $clipDocument['data'])) {
$engagementCount = $clipDocument['data']['likes'];
}
if (array_key_exists("dislikes", $clipDocument['data'])) {
$engagementCount += $clipDocument['data']['dislikes'];
}
if (array_key_exists("comments", $clipDocument['data'])) {
$engagementCount += $clipDocument['data']['comments'];
}
if (array_key_exists("shares", $clipDocument['data'])) {
$engagementCount += $clipDocument['data']['shares'];
}
$tempInfluenceData = $influenceData[$key]['data'];
if (count($tempInfluenceData) > 0) {
$sourceHashCodeKey = array_search($clipDocument['data'][$groupByField], array_column($tempInfluenceData, $groupByField));
if ($sourceHashCodeKey === false) {
$tempData = [$groupByField => $clipDocument['data'][$groupByField], 'influence' => $clipDocument['data']['source_link'],
'source_type' => $clipDocument['data']['source_publisher_type'], 'engagement' => $engagementCount, 'totalSentiment' => 0];
if (array_key_exists("sentiment", $clipDocument['data'])) {
$sentiment = 1;
$tempData['totalSentiment'] = $sentiment;
$tempData[$clipDocument['data']['sentiment']] = $sentiment;
}
array_push($tempInfluenceData, $tempData);
$influenceData[$key]['data'] = $tempInfluenceData;
} else {
if (array_key_exists("sentiment", $clipDocument['data'])) {
$influenceData[$key]['data'][$sourceHashCodeKey]['totalSentiment'] += 1;
if (array_key_exists($clipDocument['data']['sentiment'], $influenceData[$key]['data'][$sourceHashCodeKey])) {
$influenceData[$key]['data'][$sourceHashCodeKey][$clipDocument['data']['sentiment']] += 1;
} else {
$influenceData[$key]['data'][$sourceHashCodeKey][$clipDocument['data']['sentiment']] = 1;
}
}
$influenceData[$key]['data'][$sourceHashCodeKey]['engagement'] += $engagementCount;
}
} else {
$tempData = [$groupByField => $clipDocument['data'][$groupByField], 'influence' => $clipDocument['data']['source_link'],
'source_type' => $clipDocument['data']['source_publisher_type'], 'engagement' => $engagementCount, 'totalSentiment' => 0];
if (array_key_exists("sentiment", $clipDocument['data'])) {
$sentiment = 1;
$tempData['totalSentiment'] = $sentiment;
$tempData[$clipDocument['data']['sentiment']] = $sentiment;
}
$influenceData[$key]['data'][0] = $tempData;
}
}
}
}
}
}
}
}
}
foreach ($influenceData as $key => $influenceDataVal) {
usort($influenceData[$key]['data'], function ($a, $b) {
return $b['totalSentiment'] <=> $a['totalSentiment'];
});
}
foreach ($influenceData as $key => $dataVal) {
$influenceData[$key]['data'] = array_slice($dataVal['data'], 0, 10);
}
return $this->generateResponse([
'data' => $influenceData
], 200, []);
}
/**
* @param $filters
*
* @return array
*
* @throws \Exception
*/
public function getDuration($filters)
{
$duration = [];
if (count($filters) > 0) {
if (array_key_exists('date', $filters)) {
$period = new DatePeriod(
$filters['date']->getFilters()[0]->getValue(),
new DateInterval('P1D'),
$filters['date']->getFilters()[1]->getValue()
);
foreach ($period as $key => $value) {
$duration[$value->format('Y-m-d')] = 0;
}
}
}
return $duration;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,329 @@
<?php
namespace AppBundle\Controller\V1;
use ApiBundle\Controller\AbstractCRUDController;
use ApiBundle\Controller\Annotation\Roles;
use ApiBundle\Response\ViewInterface;
use ApiBundle\Security\Inspector\InspectorInterface;
use ApiDocBundle\Controller\Annotation\AppApiDoc;
use CacheBundle\Entity\Category;
use CacheBundle\Repository\CategoryRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use UserBundle\Enum\AppLimitEnum;
/**
* Class CategoryController
* @package AppBundle\Controller\V1
*
* @Route("/categories", service="app.controller.category")
*/
class CategoryController extends AbstractCRUDController
{
/**
* Move specified feed to another category.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* "/{movedId}/move_to/{destinationId}",
* requirements={
* "movedId": "\d+",
* "destinationId": "\d+"
* },
* methods={ "POST" }
* )
* @AppApiDoc(
* resource=true,
* section="Category",
* output={
* "class"="Pagination<CacheBundle\Entity\Category>",
* "groups"={ "id", "category_tree", "feed_tree" }
* },
* statusCodes={
* 200="List of updated categories successfully returned.",
* 400="Invalid data provided.",
* 403="You don't have permissions to move this category.",
* 404="Can't find moved or destination category."
* }
* )
*
* @param integer $movedId A moved Category entity id.
* @param integer $destinationId A Category entity id where the category is
* moved.
*
* @return \ApiBundle\Response\ViewInterface|\Symfony\Component\HttpFoundation\Response
*/
public function moveAction($movedId, $destinationId)
{
$movedId = (integer) $movedId;
$destinationId = (integer) $destinationId;
$userId = \app\op\invokeIf($this->getCurrentUser(), 'getId');
/** @var CategoryRepository $repository */
$repository = $this->getManager()->getRepository(Category::class);
$moved = $repository->get($movedId, $userId);
if (! $moved instanceof Category) {
return $this->generateResponse("Can't find category with id {$movedId}.", 404);
}
//
// Check that user don't try to move internal category.
//
if ($moved->isInternal()) {
return $this->generateResponse('Can\'t move internal category.', 403);
}
//
// We should don't make any changes if client try to move category into
// the same category.
//
if ($moved->getParent()->getId() !== $destinationId) {
$destination = $repository->get($destinationId, $userId, [
Category::TYPE_CUSTOM,
Category::TYPE_MY_CONTENT,
]);
if (! $destination instanceof Category) {
return $this->generateResponse("Can't find category with id {$destinationId}.", 404);
}
//
// All ok, now we need to validate destination category id.
//
$moved->setParent($destination);
/** @var ValidatorInterface $validator */
$validator = $this->get('validator');
$errors = $validator->validate($moved);
if (count($errors) > 0) {
//
// Get all violation errors and send it to client.
//
$errors = array_map(function (ConstraintViolationInterface $violation) {
return $violation->getMessage();
}, iterator_to_array($errors));
return $this->generateResponse($errors, 400);
}
//
// Validation passed, update entity.
//
$this->getManager()->persist($moved);
$this->getManager()->flush();
}
return $this->forward('app.controller.category:listAction');
}
/**
* Create new category for current user.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(methods={ "POST" })
* @AppApiDoc(
* resource=true,
* section="Category",
* input={
* "class"="CacheBundle\Form\CategoryType",
* "name"=false
* },
* output={
* "class"="CacheBundle\Entity\Category",
* "groups"={ "id", "category" }
* },
* statusCodes={
* 200="Category successfully created.",
* 400="Invalid data provided.",
* 403="You don't have permissions to create category."
* }
* )
*
* @param Request $request A Request instance.
*
* @return \CacheBundle\Entity\Category|\ApiBundle\Response\ViewInterface
*/
public function createAction(Request $request)
{
return parent::createEntity($request, new Category($this->getCurrentUser()));
}
/**
* Get list of categories for current user.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(methods={ "GET" })
* @AppApiDoc(
* section="Category",
* output={
* "class"="Pagination<CacheBundle\Entity\Category>",
* "groups"={ "id", "category_tree", "feed_tree" }
* },
* statusCodes={
* 200="List of categories successfully returned."
* }
* )
*
* @return ViewInterface
*/
public function listAction()
{
/** @var CategoryRepository $repository */
$repository = $this->getManager()->getRepository(Category::class);
$user = $this->getCurrentUser();
$categories = $repository->getList($user->getId());
$count = count($categories);
// Simulate pagination serialization.
return $this->generateResponse([
'data' => $categories,
'count' => $count,
'totalCount' => $count,
'page' => 1,
'limit' => $count,
], 200, [
'id',
'category_tree',
'feed_tree',
]);
}
/**
* Get specified category by id.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* "/{id}",
* requirements={ "id"="\d+" },
* methods={ "GET" }
* )
* @AppApiDoc(
* resource=true,
* section="Category",
* output={
* "class"="CacheBundle\Entity\Category",
* "groups"={ "id", "category", "feed_tree" }
* },
* statusCodes={
* 200="Category successfully returned.",
* 403="You don't have permissions to view this category.",
* 404="Can't find category by specified id."
* }
* )
*
* @param integer $id A Category entity id.
*
* @return \CacheBundle\Entity\Category|\ApiBundle\Response\ViewInterface
*/
public function getAction($id)
{
return parent::getEntity($id);
}
/**
* Update specified category.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* "/{id}",
* requirements={ "id"="\d+" },
* methods={ "PUT" }
* )
* @AppApiDoc(
* resource=true,
* section="Category",
* input={
* "class"="CacheBundle\Form\CategoryType",
* "name"=false
* },
* output={
* "class"="CacheBundle\Entity\Category",
* "groups"={ "id", "category" }
* },
* statusCodes={
* 200="Category successfully updated.",
* 400="Invalid data provided.",
* 403="You don't have permissions to update this category.",
* 404="Can't find category by specified id."
* }
* )
*
* @param Request $request A Request instance.
* @param integer $id A Category entity id.
*
* @return \CacheBundle\Entity\Category|\ApiBundle\Response\ViewInterface
*/
public function putAction(Request $request, $id)
{
return parent::putEntity($request, $id);
}
/**
* Delete specified category.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* "/{id}",
* requirements={ "id"="\d+" },
* methods={ "DELETE" }
* )
* @AppApiDoc(
* resource=true,
* section="Category",
* statusCodes={
* 204="Category successfully deleted.",
* 403="You don't have permissions to delete this category.",
* 404="Can't find category by specified id."
* }
* )
*
* @param integer $id A Category entity id.
*
* @return array|\ApiBundle\Response\ViewInterface
*/
public function deleteAction($id)
{
/** @var CategoryRepository $repository */
$repository = $this->getManager()->getRepository($this->entity);
$category = $repository->find($id);
if ($category === null) {
return $this->generateResponse("Can't find category with id {$id}.", 404);
}
$reasons = $this->checkAccess(InspectorInterface::DELETE, $category);
if (count($reasons) > 0) {
return $this->generateResponse($reasons, 403);
}
//
// Update restriction limit for current user only if deleted category has
// some feeds.
//
$feedCount = $repository->computeFeedCounts($id);
if ($feedCount > 0) {
$user = $this->getCurrentUser();
$user->releaseLimit(AppLimitEnum::feeds(), $feedCount);
$this->getManager()->persist($user);
}
$this->getManager()->remove($category);
$this->getManager()->flush();
return $this->generateResponse();
}
}
@@ -0,0 +1,111 @@
<?php
namespace AppBundle\Controller\V1;
use ApiBundle\Controller\Annotation\Roles;
use ApiBundle\Security\Inspector\InspectorInterface;
use ApiDocBundle\Controller\Annotation\AppApiDoc;
use CacheBundle\Entity\Comment;
use CacheBundle\Entity\Document;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Class CommentController
* @package AppBundle\Controller\V1
*
* @Route("/comments", service="app.controller.comment")
*/
class CommentController extends AbstractV1CrudController
{
/**
* Update specified comment.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* "/{commentId}",
* requirements={ "commentId"="\d+" },
* methods={ "PUT" }
* )
* @AppApiDoc(
* section="Comment",
* resource=false,input={
* "class"="CacheBundle\Form\CommentType",
* "name"=false
* },
* output={
* "class"="CacheBundle\Entity\Comment",
* "groups"={ "comment", "id" }
* },
* statusCodes={
* 200="Comment successfully updated.",
* 400="Invalid data provided.",
* 403="You don't have permissions to update this comment.",
* 404="Can't find comment by specified id."
* }
* )
*
* @param Request $request A Request instance.
* @param integer $commentId A one of comment entity id.
*
* @return \ApiBundle\Entity\ManageableEntityInterface|\ApiBundle\Response\ViewInterface
*/
public function putAction(Request $request, $commentId)
{
return parent::putEntity($request, $commentId);
}
/**
* Delete specified comment.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* "/{commentId}",
* requirements={ "commentId"="\d+" },
* methods={ "DELETE" }
* )
* @AppApiDoc(
* section="Comment",
* resource=false,
* statusCodes={
* 204="Comment successfully deleted.",
* 403="You don't have permissions to delete this comment.",
* 404="Can't find comment by specified id."
* }
* )
*
* @param integer $commentId A Comment entity id.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function deleteAction($commentId)
{
$entity = $this->em->getRepository(Comment::class)->find($commentId);
if ($entity === null) {
return $this->generateResponse("Can't find comment with id {$commentId}.", 404);
}
$reasons = $this->checkAccess(InspectorInterface::DELETE, $entity);
if (count($reasons) > 0) {
return $this->generateResponse($reasons, 403);
}
$this->em->getRepository(Document::class)
->createQueryBuilder('Document')
->update()
->set('Document.commentsCount', 'Document.commentsCount - 1')
->where('Document.id = :id')
->setParameter('id', \app\op\invokeIf($entity->getDocument(), 'getId'))
->getQuery()
->execute();
$this->em->remove($entity);
$this->em->flush();
return $this->generateResponse();
}
}
@@ -0,0 +1,277 @@
<?php
namespace AppBundle\Controller\V1;
use ApiBundle\Controller\Annotation\Roles;
use ApiBundle\Security\AccessChecker\AccessCheckerInterface;
use ApiBundle\Security\Inspector\InspectorInterface;
use ApiDocBundle\Controller\Annotation\AppApiDoc;
use AppBundle\Controller\Traits\AccessCheckerTrait;
use AppBundle\Controller\Traits\FormFactoryAwareTrait;
use AppBundle\Controller\Traits\TokenStorageAwareTrait;
use AppBundle\Entity\EmailedDocument;
use AppBundle\Form\EmailedDocumentType;
use CacheBundle\Comment\Manager\CommentManagerInterface;
use CacheBundle\Entity\Comment;
use CacheBundle\Entity\Document;
use CacheBundle\Repository\CommentRepository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Pagination\Paginator;
use OldSound\RabbitMqBundle\RabbitMq\ProducerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
/**
* Class DocumentController
* @package AppBundle\Controller\V1
*
* @Route("/documents", service="app.controller.document")
*/
class DocumentController extends AbstractV1Controller
{
use
TokenStorageAwareTrait,
FormFactoryAwareTrait,
AccessCheckerTrait;
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var CommentManagerInterface
*/
private $commentManager;
/**
* @var ProducerInterface
*/
private $emailProducer;
/**
* DocumentController constructor.
*
* @param TokenStorageInterface $tokenStorage A TokenStorageInterface
* instance.
* @param FormFactoryInterface $formFactory A FormFactoryInterface
* instance.
* @param AccessCheckerInterface $accessChecker A AccessCheckerInterface
* instance.
* @param EntityManagerInterface $em A EntityManagerInterface
* instance.
* @param CommentManagerInterface $commentManager A CommentManagerInterface
* instance.
* @param ProducerInterface $emailProducer A producer interface for
* emailing documents.
*/
public function __construct(
TokenStorageInterface $tokenStorage,
FormFactoryInterface $formFactory,
AccessCheckerInterface $accessChecker,
EntityManagerInterface $em,
CommentManagerInterface $commentManager,
ProducerInterface $emailProducer
) {
$this->tokenStorage = $tokenStorage;
$this->formFactory = $formFactory;
$this->accessChecker = $accessChecker;
$this->em = $em;
$this->commentManager = $commentManager;
$this->emailProducer = $emailProducer;
}
/**
* Create new comment for specified document.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* "/{documentId}/comments",
* requirements={ "documentId"="\d+" },
* methods={ "POST" }
* )
* @AppApiDoc(
* section="Document",
* resource=true,
* input={
* "class"="CacheBundle\Form\CommentType",
* "name"=false
* },
* output={
* "class"="CacheBundle\Entity\Comment",
* "groups"={ "comment", "id" }
* },
* statusCodes={
* 200="Comment successfully saved.",
* 400="Invalid data provided."
* }
* )
*
* @param Request $request A Request instance.
* @param integer $documentId Commented Document entity id.
*
* @return \ApiBundle\Entity\ManageableEntityInterface|\ApiBundle\Response\ViewInterface
*/
public function createCommentAction(Request $request, $documentId)
{
$document = $this->em->getRepository(Document::class)->find($documentId);
if (! $document instanceof Document) {
return $this->generateResponse([[
'message' => 'Document not found',
'transKey' => 'commentDocumentInvalidDocument',
'type' => 'error',
'parameters' => [ 'current' => $documentId ],
], ], 404);
}
$comment = new Comment($this->getCurrentUser(), '');
$form = $this->createForm($comment->getCreateFormClass(), $comment);
// Submit data into form.
$form->submit($request->request->all());
if ($form->isValid()) {
//
// Check that current user can create this entity.
// If user don't have rights to create this entity we should send all
// founded restrictions to client.
//
$reasons = $this->checkAccess(InspectorInterface::CREATE, $comment);
if (count($reasons) > 0) {
//
// User don't have rights to create this entity so send all
// founded restriction reasons to client.
//
return $this->generateResponse($reasons, 403);
}
$this->commentManager->addComment($comment, $document);
$this->em->persist($comment);
$this->em->flush();
return $comment;
}
// Client send invalid data.
return $this->generateResponse($form, 400);
}
/**
* Get list of comments for specified document.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* "/{documentId}/comments",
* requirements={ "documentId"="\d+" },
* methods={ "GET" }
* )
* @AppApiDoc(
* section="Document",
* resource=true,
* filters={
* {
* "name"="offset",
* "dataType"="integer",
* "description"="Offset from beginning of collection, start from 1",
* "requirements"="\d+",
* "default"="1"
* },
* {
* "name"="limit",
* "dataType"="integer",
* "description"="Max entities per page, default 10",
* "requirements"="\d+",
* "default"="10"
* },
* },
* output={
* "class"="Paginated<CacheBundle\Entity\Comment>",
* "groups"={ "comment", "id" }
* },
* statusCodes={
* 200="List of comments returned.",
* 404="Invalid document id."
* }
* )
* @param Request $request A Request instance.
* @param integer $documentId A Document entity id.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function getCommentsAction(Request $request, $documentId)
{
$document = $this->em->getRepository(Document::class)->find($documentId);
if (! $document instanceof Document) {
return $this->generateResponse([[
'message' => 'Document not found',
'transKey' => 'getDocumentCommentsInvalidDocument',
'type' => 'error',
'parameters' => [ 'current' => $documentId ],
], ], 404);
}
/** @var CommentRepository $repository */
$repository = $this->em->getRepository(Comment::class);
$qb = $repository->getListForDocument($documentId);
$offset = $request->query->getInt('offset', CommentManagerInterface::NEW_COMMENT_POOL_SIZE);
$limit = $request->query->getInt('limit', 10);
$qb
->setFirstResult($offset)
->setMaxResults($limit);
return $this->generateResponse(new Paginator($qb), 200, [ 'id', 'comment' ]);
}
/**
* Send specified documents content to recipients.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route(
* "/email",
* methods={ "POST" }
* )
* @AppApiDoc(
* section="Document",
* resource=true,
* input={
* "class"="AppBundle\Form\EmailedDocumentType",
* "name"=false
* },
* statusCodes={
* 204="Email's sent.",
* 400="Invalid data."
* }
* )
* @param Request $request A Request instance.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function emailAction(Request $request)
{
$emailedDocument = new EmailedDocument();
$form = $this->createForm(EmailedDocumentType::class, $emailedDocument);
$form->submit($request->request->all());
if ($form->isSubmitted() && $form->isValid()) {
$this->em->persist($emailedDocument);
$this->em->flush();
$this->emailProducer->publish($emailedDocument->getId());
return $this->generateResponse();
}
return $this->generateResponse($form, 400);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,269 @@
<?php
namespace AppBundle\Controller\V1;
use ApiBundle\Controller\Annotation\Roles;
use AppBundle\Controller\Traits\FormFactoryAwareTrait;
use AppBundle\Controller\Traits\TokenStorageAwareTrait;
use AppBundle\Exception\LimitExceedException;
use AppBundle\Form\SearchRequest\SimpleQuerySearchRequestType;
use AppBundle\Manager\SimpleQuery\SimpleQueryManagerInterface;
use AppBundle\Manager\Source\SourceManagerInterface;
use CacheBundle\Document\Extractor\DocumentContentExtractorInterface;
use Common\Enum\FieldNameEnum;
use Doctrine\ORM\EntityManagerInterface;
use IndexBundle\Model\ArticleDocumentInterface;
use IndexBundle\SearchRequest\SearchRequestBuilderInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use UserBundle\Enum\AppLimitEnum;
use UserBundle\Enum\ThemeOptionExtractEnum;
/**
* Class QueryController
* @package AppBundle\Controller\V1
*
* @Route("/query", service="app.controller.query")
*/
class QueryController extends AbstractV1Controller
{
use
FormFactoryAwareTrait,
TokenStorageAwareTrait;
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var SourceManagerInterface
*/
private $sourceManager;
/**
* @var SimpleQueryManagerInterface
*/
private $queryManager;
/**
* @var DocumentContentExtractorInterface
*/
private $extractor;
/**
* QueryController constructor.
*
* @param FormFactoryInterface $formFactory A
* FormFactoryInterface
* instance.
* @param EntityManagerInterface $em A
* RestrictionsRepositoryInterface
* instance.
* @param TokenStorageInterface $tokenStorage A
* TokenStorageInterface
* instance.
* @param SourceManagerInterface $sourceManager A
* SourceManagerInterface
* instance.
* @param SimpleQueryManagerInterface $queryManager A
* SimpleQueryManagerInterface
* instance.
* @param DocumentContentExtractorInterface $extractor A
* DocumentContentExtractorInterface
* instance.
*/
public function __construct(
FormFactoryInterface $formFactory,
EntityManagerInterface $em,
TokenStorageInterface $tokenStorage,
SourceManagerInterface $sourceManager,
SimpleQueryManagerInterface $queryManager,
DocumentContentExtractorInterface $extractor
) {
$this->formFactory = $formFactory;
$this->em = $em;
$this->tokenStorage = $tokenStorage;
$this->sourceManager = $sourceManager;
$this->queryManager = $queryManager;
$this->extractor = $extractor;
}
/**
* Make simple search without saving query in database.
* Fetched documents are cached but not indexed.
*
* @Roles("ROLE_SUBSCRIBER")
*
* @Route("/search", methods={ "POST" })
* @ApiDoc(
* resource="Search",
* section="Query",
* input={
* "class"="AppBundle\Form\SearchRequest\SimpleQuerySearchRequestType",
* "name"=false
* },
* output={
* "class"="",
* "data"={
* "documents"={
* "class"="Pagination<CacheBundle\Entity\Document>",
* "groups"={ "document" }
* },
* "advancedFilters"={
* "dataType"="array",
* "required"=true,
* "readonly"=true,
* "description"="Array of advanced filters values for this search request."
* },
* "stats"={
* "dataType"="object",
* "required"=true,
* "readonly"=true,
* "description"="Internal statistics, showed only in staging and local developers machine.",
* "children"={
* "totalOnPage"={
* "dataType"="integer",
* "required"=true,
* "readonly"=true,
* "description"="Total founded document on current page."
* },
* "newDocuments"={
* "dataType"="integer",
* "required"=true,
* "readonly"=true,
* "description"="Number of documents that were not in our database."
* },
* "alreadyExistsDocuments"={
* "dataType"="integer",
* "required"=true,
* "readonly"=true,
* "description"="Number of documents that already in our database."
* },
* "fromCache"={
* "dataType"="boolean",
* "required"=true,
* "readonly"=true,
* "description"="Flag, all documents fetched from our internal cache if set."
* },
* "expiresAt"={
* "dataType"="datetime",
* "required"=true,
* "readonly"=true,
* "description"="When this query is expired."
* }
* }
* }
* }
* },
* statusCodes={
* 200="Search completed.",
* 400="Invalid data provided"
* }
* )
*
* @param Request $request A Request instance.
*
* @return array|\ApiBundle\Response\ViewInterface
*/
public function searchAction(Request $request)
{
$form = $this->createForm(SimpleQuerySearchRequestType::class);
$form->submit($request->request->all());
if ($form->isValid()) {
$user = $this->getCurrentUser();
try {
$user->useLimit(AppLimitEnum::searches());
} catch (LimitExceedException $exception) {
return $this->generateResponse([
'failedRestriction' => AppLimitEnum::SEARCHES,
'restrictions' => $user->getRestrictions(),
], 402);
}
$this->em->persist($user);
$this->em->flush();
/** @var SearchRequestBuilderInterface $builder */
$builder = $form->getData();
$searchRequest = $builder
->setFields([
FieldNameEnum::TITLE,
FieldNameEnum::MAIN,
])
->addSort(FieldNameEnum::PUBLISHED, 'desc')
->build();
$response = $this->queryManager->searchAndCache(
$searchRequest,
$request->request->get('filters', []),
$request->request->get('advancedFilters', [])
);
$query = $response->getQuery();
$response->mapDocuments(function (ArticleDocumentInterface $document) use ($query) {
return $document->mapNormalizedData(function (array $data) use ($query) {
$result = $this->extractor->extract(
$data['content'],
$query->getRaw(),
ThemeOptionExtractEnum::start(),
true
);
$data['content'] = $result->getText() . (
mb_strlen($data['content']) > $result->getLength()
? '...'
: ''
);
return $data;
});
});
$result = [
'documents' => $this->paginate($response, $builder->getPage(), $builder->getLimit()),
'advancedFilters' => $searchRequest->getAvailableAdvancedFilters() ?: (object) [],
];
//
// Return internal statistic.
//
$result['stats'] = [
'newDocuments' => $response->getUniqueCount(),
'alreadyExistsDocuments' => $response->count() - $response->getUniqueCount(),
'fromCache' => $response->isFromCache(),
'expiresAt' => $query->getExpirationDate()->format('c'),
];
//
// Return meta information about query.
//
$sources = $this->sourceManager->getSourcesForQuery($query, [ 'id', 'title', 'type' ]);
$sourceLists = $this->sourceManager->getSourceListsForQuery($query, [ 'id', 'name' ]);
$result['meta'] = [
'type' => 'query',
'status' => 'synced',
'search' => [
'query' => $query->getRaw(),
'filters' => $query->getRawFilters() ?: (object) [],
'advancedFilters' => $query->getRawAdvancedFilters() ?: (object) [],
],
'sources' => $sources,
'sourceLists' => $sourceLists,
];
return $this->generateResponse($result);
}
return $this->generateResponse($form, 400);
}
}
@@ -0,0 +1,313 @@
<?php
namespace AppBundle\Controller\V1;
use AppBundle\Controller\Traits\FormFactoryAwareTrait;
use AppBundle\Controller\Traits\TokenStorageAwareTrait;
use AppBundle\Manager\Source\SourceManagerInterface;
use CacheBundle\Entity\SourceList;
use CacheBundle\Form\Sources\SourceSearchType;
use CacheBundle\Repository\SourceListRepository;
use Doctrine\ORM\EntityManagerInterface;
use IndexBundle\Model\SourceDocument;
use IndexBundle\SearchRequest\SearchRequestBuilder;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use ApiBundle\Controller\Annotation\Roles;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
/**
* Class SourceIndexController
* @package AppBundle\Controller\V1
*
* @Route("/source-index", service="app.controller.source-index")
*/
class SourceIndexController extends AbstractV1Controller
{
use
TokenStorageAwareTrait,
FormFactoryAwareTrait;
/**
* @var SourceManagerInterface
*/
private $sourceManager;
/**
* @var EntityManagerInterface
*/
private $em;
/**
* SourceIndexController constructor.
*
* @param TokenStorageInterface $tokenStorage A TokenStorageInterface
* instance.
* @param FormFactoryInterface $formFactory A FormFactoryInterface
* instance.
* @param SourceManagerInterface $sourceManager A SourceManagerInterface
* instance.
* @param EntityManagerInterface $em A EntityManagerInterface
* instance.
*/
public function __construct(
TokenStorageInterface $tokenStorage,
FormFactoryInterface $formFactory,
SourceManagerInterface $sourceManager,
EntityManagerInterface $em
) {
$this->tokenStorage = $tokenStorage;
$this->formFactory = $formFactory;
$this->sourceManager = $sourceManager;
$this->em = $em;
}
/**
* Fetch all sources from our cache.
*
* @Route("/", methods={ "POST" })
*
* @ApiDoc(
* resource=true,
* section="Source Index",
* input={
* "class"="CacheBundle\Form\Sources\SourceSearchType",
* "name"=false
* },
* output={
* "class"="Pagination<IndexBundle\Model\SourceDocument>",
* "groups"={ "id", "source" }
* }
* )
*
* @param Request $request A Request instance.
*
* @return \Knp\Component\Pager\Pagination\PaginationInterface|\ApiBundle\Response\ViewInterface
*/
public function listAction(Request $request)
{
$form = $this->createForm(SourceSearchType::class);
$form->submit($request->request->all());
if ($form->isValid()) {
/** @var SearchRequestBuilder $searchRequestBuilder */
$searchRequestBuilder = $form->getData();
$searchRequestBuilder->setUser($this->getCurrentUser());
$response = $this->sourceManager->find($searchRequestBuilder);
$advancedFilters = $this->sourceManager->getAvailableFilters($searchRequestBuilder);
$sort = $searchRequestBuilder->getSorts();
$sort = [
'field' => array_search(key($sort), SourceSearchType::$fields),
'direction' => current($sort),
];
return $this->generateResponse([
'sources' => $this->paginate($response, $searchRequestBuilder->getPage(), $searchRequestBuilder->getLimit()),
'advancedFilters' => $advancedFilters ?: (object) [],
'meta' => [
'query' => $request->request->get('query'),
'advancedFilters' => $request->request->get('advancedFilters', [])?: (object) [],
'sort' => $sort,
],
], 200, [ 'id', 'source' ]);
}
return $this->generateResponse($form, 400);
}
/**
* Replace source lists for specified source.
*
* @Route("/{id}/list", methods={ "POST" })
* @Roles("ROLE_SUBSCRIBER")
*
* @ApiDoc(
* resource=true,
* section="Source Index",
* parameters={
* "sourceList"={
* "name"="sourceLists",
* "dataType"="array",
* "description"="Array of source lists ids."
* }
* }
* )
*
* @param Request $request A Request instance.
* @param integer $id A Source entity id.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function replaceListAction(Request $request, $id)
{
if (count($this->sourceManager->getIndex()->has($id)) > 0) {
return $this->generateResponse([ [
'message' => "Can't find source with id {$id}",
'transKey' => 'replaceSourceUnknown',
'type' => 'error',
'parameters' => [ 'current' => $id ],
], ], 404);
}
$user = $this->getCurrentUser();
$sourceLists = $request->request->get('sourceLists');
if (! is_array($sourceLists)) {
return $this->generateResponse([[
'message' => 'sourceLists: This value should not be empty.',
'transKey' => 'replaceSourceListsEmpty',
'type' => 'error',
'parameters' => [ 'current' => null ],
], ], 400);
}
/** @var SourceListRepository $repository */
$repository = $this->em->getRepository(SourceList::class);
$foundedIds = $repository->sanitizeIds($sourceLists, $user->getId());
if (count($foundedIds) !== count($sourceLists)) {
//
// Some of provided id is not found or not owned by current user.
//
return $this->generateResponse([ [
'message' => 'sourceLists: This value is invalid.',
'transKey' => 'replaceSourceListInvalid',
'type' => 'error',
'parameters' => [
'current' => $sourceLists,
'invalid' => array_diff($sourceLists, $foundedIds),
],
], ], 400);
}
$this->sourceManager->replaceRelation($id, $foundedIds);
return $this->generateResponse();
}
/**
* Add Sources to Sources lists
*
* @Route("/add-to-sources-list", methods={ "POST" })
*
* @Roles("ROLE_SUBSCRIBER")
*
* @ApiDoc(
* resource=true,
* section="Source Index",
* parameters={
* "sources"={
* "name"="sources",
* "dataType"="integer",
* "actualType"="collection",
* "required"=true,
* "description"="Array of Source id."
* },
* "sourceLists"={
* "name"="sourceLists",
* "dataType"="integer",
* "actualType"="collection",
* "required"=true,
* "description"="Array of Sources Lists id."
* },
* }
* )
*
* @param Request $request A Request instance.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function addToSourceListAction(Request $request)
{
$sources = (array) $request->request->get('sources', []);
$sourceLists = (array) $request->request->get('sourceLists', []);
//
// Check that all fields are provided.
//
if (count($sources) === 0) {
return $this->generateResponse(
[
[
'message' => 'Sources should be selected.',
'transKey' => 'sourceToListsSourcesEmpty',
'type' => 'error',
'parameters' => [
'current' => $sources,
],
],
],
400
);
}
if (count($sourceLists) === 0) {
return $this->generateResponse(
[
[
'message' => 'Source lists should be selected.',
'transKey' => 'sourceToListsSourceListsEmpty',
'type' => 'error',
'parameters' => [
'current' => $sourceLists,
],
],
],
400
);
}
$user = $this->getCurrentUser();
//
// Validate specified sources and source lists ids.
//
/** @var SourceListRepository $repository */
$repository = $this->em->getRepository('CacheBundle:SourceList');
$existsSources = $this->sourceManager->getIndex()->get($sources, 'id');
$existsSources = array_map(function (SourceDocument $document) {
return $document['id'];
}, $existsSources);
if (count($sources) !== count($existsSources)) {
return $this->generateResponse(
[
[
'message' => 'sources: This value is invalid.',
'transKey' => 'sourceToListsSourcesInvalid',
'type' => 'error',
'parameters' => $sources,
],
],
400
);
}
$existsSourceLists = $repository->sanitizeIds($sourceLists, $user->getId());
if (count($sourceLists) !== count($existsSourceLists)) {
return $this->generateResponse(
[
[
'message' => 'sourceLists: This value is invalid.',
'transKey' => 'sourceToListsSourceListsInvalid',
'type' => 'error',
'parameters' => $sourceLists,
],
],
400
);
}
$this->sourceManager->bindSourcesToLists($user, $sources, $sourceLists);
return $this->generateResponse();
}
}
@@ -0,0 +1,486 @@
<?php
namespace AppBundle\Controller\V1;
use ApiBundle\Controller\AbstractApiController;
use ApiBundle\Security\Inspector\InspectorInterface;
use AppBundle\AppBundleServices;
use AppBundle\Manager\Source\SourceManagerInterface;
use CacheBundle\CacheBundleServices;
use CacheBundle\Entity\SourceList;
use CacheBundle\Entity\SourceToSourceList;
use CacheBundle\Form\Sources\SourceListSearchType;
use CacheBundle\Form\Sources\SourceListType;
use CacheBundle\Form\Sources\SourceSearchType;
use CacheBundle\Repository\SourceListRepository;
use CacheBundle\Security\Inspector\SourceListInspector;
use IndexBundle\SearchRequest\SearchRequestBuilder;
use Knp\Component\Pager\PaginatorInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use ApiBundle\Controller\Annotation\Roles;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Class SourceIndexController
* @package AppBundle\Controller\V1
*
* @Route("/source-list", service="app.controller.source-list")
*/
class SourceListController extends AbstractApiController
{
/**
* Get list of sources for the user
*
* @Route("/list", methods={ "POST" })
* @Roles("ROLE_SUBSCRIBER")
*
* @ApiDoc(
* resource=true,
* section="Source List",
* input={
* "class"="CacheBundle\Form\Sources\SourceListSearchType",
* "name"=false
* },
* output={
* "class"="Pagination<CacheBundle\Entity\SourceList>",
* "groups"={ "id", "source_list" }
* }
* )
*
* @param Request $request A Request instance.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function indexAction(Request $request)
{
$form = $this->createForm(SourceListSearchType::class);
$form->submit($request->request->all());
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$page = (int) $data['page'];
$limit = (int) $data['limit'];
$onlyShared = (boolean) $data['onlyShared'];
$user = $this->getCurrentUser();
$em = $this->getManager();
/** @var SourceListRepository $sourceListRepository */
$sourceListRepository = $em->getRepository(SourceList::class);
/** @var PaginatorInterface $paginator */
$paginator = $this->get('knp_paginator');
$qb = $sourceListRepository->getSourcesListsQB($user->getId(), $data['sort'], $onlyShared);
$pagination = $paginator->paginate(
$qb,
$page,
$limit
);
$sort = $data['sort'] ;
$sort = [
'field' => array_search(key($sort), SourceListSearchType::$fields),
'direction' => current($sort),
];
/** @var NormalizerInterface $normalizer */
$normalizer = $this->get('serializer');
$result = $normalizer->normalize($pagination, null, ['id', 'source_list']);
$result['sort'] = $sort;
return $this->generateResponse($result, 200);
}
return $this->generateResponse($form, 400);
}
/**
* Create a source list
*
* @Route("/", methods={ "POST" })
*
* @Roles("ROLE_SUBSCRIBER")
*
* @ApiDoc(
* resource=true,
* section="Source List",
* input={
* "class"="CacheBundle\Form\Sources\SourceListType",
* "name"=false
* },
* output={
* "class"="CacheBundle\Entity\SourceList",
* }
* )
*
* @param Request $request A Request entity instance.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function createAction(Request $request)
{
$sourceList = new SourceList();
$sourceList->setUser($this->getCurrentUser());
$form = $this->createForm(SourceListType::class, $sourceList);
$form->submit($request->request->all());
if ($form->isValid()) {
$em = $this->getManager();
$em->persist($sourceList);
$em->flush();
return $this->generateResponse($sourceList, 200, [
'source_list',
'id',
]);
}
return $this->generateResponse($form, 400);
}
/**
* Rename a source list
*
* @Route(
* "/{id}",
* requirements={ "id": "\d+" },
* methods={ "PUT" }
* )
*
* @Roles("ROLE_SUBSCRIBER")
*
* @ApiDoc(
* resource=true,
* section="Source List",
* parameters={
* "name"={
* "name"="name",
* "dataType"="string",
* "required"="true",
* "description"="A new name of the source list"
* }
* }
* )
*
* @param Request $request A HTTP Request instance.
* @param SourceList $sourceList A updated SourceList instance.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function updateAction(Request $request, SourceList $sourceList)
{
$form = $this->createForm(SourceListType::class, $sourceList);
$form->submit($request->request->all());
if ($form->isValid()) {
$em = $this->getManager();
$reasons = $this->checkAccess(InspectorInterface::UPDATE, $sourceList);
if (count($reasons) > 0) {
return $this->generateResponse($reasons, 403);
}
$sourceList->setUpdatedBy($this->getCurrentUser());
$em->persist($sourceList);
$em->flush();
return $this->generateResponse($sourceList, 200, [
'source_list',
'id',
]);
}
return $this->generateResponse($form, 400);
}
/**
* Delete a source list
*
* @Route("/{id}",
* requirements={ "id": "\d+" },
* methods={ "DELETE" }
* )
*
* @Roles("ROLE_SUBSCRIBER")
*
* @ApiDoc(
* resource=true,
* section="Source List",
* parameters={
* "id"={
* "name"="id",
* "dataType"="integer",
* "required"="true",
* "description"="Id of the source list which changing"
* }
* }
* )
*
* @param SourceList $sourceList A deleted SourceList instance.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function deleteAction(SourceList $sourceList)
{
$reasons = $this->checkAccess(InspectorInterface::DELETE, $sourceList);
if (count($reasons) > 0) {
return $this->generateResponse($reasons, 403);
}
//
// Remove this source list from all sources which is contains in it.
//
/** @var SourceManagerInterface $sourceManager */
$sourceManager = $this->container->get(CacheBundleServices::SOURCE_CACHE);
$sourceManager->unbindSourcesFromLists($sourceList->getId());
$em = $this->getManager();
$em->remove($sourceList);
$em->flush();
return $this->generateResponse();
}
/**
* Get list of sources for specified source list.
*
* @Route("/{id}/sources/search",
* requirements={ "id": "\d+" },
* methods={ "POST" }
* )
*
* @Roles("ROLE_SUBSCRIBER")
*
* @ApiDoc(
* resource="Sources of specified source list",
* section="Source List",
* input={
* "class"="CacheBundle\Form\Sources\SourceSearchType",
* "name"=false
* }
* )
*
* @param Request $request A Request instance.
* @param integer $id A SourceList entity id.
*
* @return \Knp\Component\Pager\Pagination\PaginationInterface|\ApiBundle\Response\ViewInterface
*/
public function sourcesAction(Request $request, $id)
{
$user = $this->getCurrentUser();
/** @var SourceListRepository $repository */
$repository = $this->getManager()->getRepository('CacheBundle:SourceList');
$sourceList = $repository->getSourcesLists($id, $user->getId());
if ($sourceList === null) {
return $this->generateResponse("Can't find source list with id $id", 404);
}
$form = $this->createForm(SourceSearchType::class);
$form->submit($request->request->all());
if ($form->isSubmitted() && $form->isValid()) {
/** @var SearchRequestBuilder $searchRequestBuilder */
$searchRequestBuilder = $form->getData();
/** @var SourceManagerInterface $manager */
$manager = $this->get(AppBundleServices::SOURCE_MANAGER);
$searchRequestBuilder->setUser($user);
$response = $manager->find($searchRequestBuilder, $sourceList);
/** @var PaginatorInterface $paginator */
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$response,
$searchRequestBuilder->getPage(),
$searchRequestBuilder->getLimit()
);
$sort = $searchRequestBuilder->getSorts();
$sort = [
'field' => array_search(key($sort), SourceSearchType::$fields),
'direction' => current($sort),
];
return $this->generateResponse([
'sources' => $pagination,
'filters' => $request->request->get('filters', (object) []),
'sort' => $sort,
], 200, [ 'id', 'source' ]);
}
return $this->generateResponse($form, 400);
}
/**
* Clone current list.
*
* @Route("/{id}/clone",
* requirements={ "id": "\d+" },
* methods={ "POST" }
* )
* @Roles("ROLE_SUBSCRIBER")
*
* @ApiDoc(
* resource="Clone specified source list",
* section="Source List",
* parameters={
* {
* "name"="name",
* "dataType"="string",
* "required"="true"
* }
* },
* output={
* "class"="CacheBundle\Entity\SourceList",
* "groups"={ "id", "source_list" }
* }
* )
*
* @param Request $request A Request instance.
* @param integer $id A SourceList entity id.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function cloneAction(Request $request, $id)
{
$user = $this->getCurrentUser();
/** @var SourceListRepository $repository */
$repository = $this->getManager()->getRepository('CacheBundle:SourceList');
$sourceList = $repository->getSourcesLists($id, $user->getId());
$em = $this->getManager();
$name = $request->request->get('name');
if ($name === null) {
return $this->generateResponse('Required field \'name\' is not provided or empty.');
}
if ($sourceList === null) {
return $this->generateResponse("Can't find source list with id $id", 404);
}
$clone = $sourceList->cloneList();
$clone->setName($name);
/** @var SourceManagerInterface $sourceManager */
$sourceManager = $this->get(CacheBundleServices::SOURCE_CACHE);
$sources = $sourceList->getSources()->map(function (SourceToSourceList $source) {
return $source->getSource();
})->toArray();
$em->persist($clone);
$em->flush();
//
// We should add and original id 'cause otherwise he lost his binding.
//
$sourceManager->bindSourcesToLists($user, $sources, [ $sourceList->getId(), $clone->getId() ]);
return $this->generateResponse($clone, 200, [
'source_list',
'id',
]);
}
/**
* Share specified source list.
*
* @Route("/{id}/share",
* requirements={ "id": "\d+" },
* methods={ "POST" }
* )
* @Roles("ROLE_SUBSCRIBER")
*
* @ApiDoc(
* resource="Sharing",
* section="Source List"
* )
*
* @param string $id A SourceList entity id.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function shareAction($id)
{
$user = $this->getCurrentUser();
$em = $this->getManager();
/** @var SourceListRepository $repository */
$repository = $em->getRepository('CacheBundle:SourceList');
$sourceList = $repository->getSourcesLists($id, $user->getId());
if ($sourceList === null) {
return $this->generateResponse("Can't find source list with id $id", 404);
}
$reasons = $this->checkAccess(SourceListInspector::SHARE, $sourceList);
if (count($reasons) > 0) {
return $this->generateResponse($reasons, 403);
}
if (! $sourceList->getIsGlobal()) {
$sourceList
->setUpdatedBy($this->getCurrentUser())
->setIsGlobal(true);
$em->persist($sourceList);
$em->flush();
}
return $this->generateResponse();
}
/**
* Unshare specified source list.
*
* @Route("/{id}/unshare",
* requirements={ "id": "\d+" },
* methods={ "POST" }
* )
* @Roles("ROLE_SUBSCRIBER")
*
* @ApiDoc(
* resource="Sharing",
* section="Source List"
* )
*
* @param string $id A SourceList entity id.
*
* @return \ApiBundle\Response\ViewInterface
*/
public function unshareAction($id)
{
$user = $this->getCurrentUser();
$em = $this->getManager();
/** @var SourceListRepository $repository */
$repository = $em->getRepository('CacheBundle:SourceList');
$sourceList = $repository->getSourcesLists($id, $user->getId());
if ($sourceList === null) {
return $this->generateResponse("Can't find source list with id $id", 404);
}
$reasons = $this->checkAccess(SourceListInspector::UNSHARE, $sourceList);
if (count($reasons) > 0) {
return $this->generateResponse($reasons, 403);
}
if ($sourceList->getIsGlobal()) {
$sourceList
->setUpdatedBy($this->getCurrentUser())
->setIsGlobal(false);
$em->persist($sourceList);
$em->flush();
}
return $this->generateResponse();
}
}
@@ -0,0 +1,44 @@
<?php
namespace AppBundle\DataFixtures;
use IndexBundle\Fixture\IndexFixtureInterface;
use IndexBundle\Model\Generator\ExternalDocumentGenerator;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
/**
* Class AbstractExternalFixture
* Base class for external index fixtures.
*
* @package AppBundle\DataFixtures\Index
*/
abstract class AbstractExternalFixture implements
IndexFixtureInterface,
ContainerAwareInterface
{
use BaseFixtureTrait;
/**
* @var ExternalDocumentGenerator
*/
protected $generator;
/**
* AbstractExternalFixture constructor.
*/
public function __construct()
{
$this->generator = new ExternalDocumentGenerator();
}
/**
* Return index type for this fixture.
*
* @return string
*/
public function getIndex()
{
return self::INDEX_EXTERNAL;
}
}
@@ -0,0 +1,32 @@
<?php
namespace AppBundle\DataFixtures;
use Doctrine\Common\DataFixtures\AbstractFixture as BaseClass;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\ORM\EntityManager;
use Faker\Factory;
use Faker\Generator;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
/**
* Class AbstractFixture
* @package AppBundle\DataFixtures\ORM
*/
abstract class AbstractFixture extends BaseClass implements
ContainerAwareInterface,
OrderedFixtureInterface
{
use BaseFixtureTrait;
/**
* Get the order of this fixture.
*
* @return integer
*/
public function getOrder()
{
return 1;
}
}
@@ -0,0 +1,45 @@
<?php
namespace AppBundle\DataFixtures;
use IndexBundle\Fixture\IndexFixtureInterface;
use IndexBundle\Model\Generator\ExternalDocumentGenerator;
use IndexBundle\Model\Generator\InternalDocumentGenerator;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
/**
* Class AbstractInternalFixture
* Base class for internal index fixtures.
*
* @package AppBundle\DataFixtures\Index
*/
abstract class AbstractInternalIndexFixture implements
IndexFixtureInterface,
ContainerAwareInterface
{
use BaseFixtureTrait;
/**
* @var ExternalDocumentGenerator
*/
protected $generator;
/**
* AbstractExternalFixture constructor.
*/
public function __construct()
{
$this->generator = new InternalDocumentGenerator();
}
/**
* Return index type for this fixture.
*
* @return string
*/
public function getIndex()
{
return self::INDEX_INTERNAL;
}
}
@@ -0,0 +1,44 @@
<?php
namespace AppBundle\DataFixtures;
use IndexBundle\Fixture\IndexFixtureInterface;
use IndexBundle\Model\Generator\SourceDocumentGenerator;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
/**
* Class AbstractSourceIndexFixture
* Base class for source index fixtures.
*
* @package AppBundle\DataFixtures\Index
*/
abstract class AbstractSourceIndexFixture implements
IndexFixtureInterface,
ContainerAwareInterface
{
use BaseFixtureTrait;
/**
* @var SourceDocumentGenerator
*/
protected $generator;
/**
* AbstractExternalFixture constructor.
*/
public function __construct()
{
$this->generator = new SourceDocumentGenerator();
}
/**
* Return index type for this fixture.
*
* @return string
*/
public function getIndex()
{
return self::INDEX_SOURCE;
}
}
@@ -0,0 +1,50 @@
<?php
namespace AppBundle\DataFixtures;
use Faker\Factory;
use Faker\Generator;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
/**
* Class BaseFixtureTrait
* @package AppBundle\DataFixtures\ORM
*/
trait BaseFixtureTrait
{
use ContainerAwareTrait;
/**
* @var Generator
*/
private $faker;
/**
* Check current application environment.
*
* @param string|string[] $expected Expected environment name or array of
* names.
*
* @return boolean
*/
protected function checkEnvironment($expected)
{
$expected = (array) $expected;
$environment = $this->container->getParameter('kernel.environment');
return in_array($environment, $expected, true);
}
/**
* @return Generator
*/
protected function getFaker()
{
if ($this->faker === null) {
$this->faker = Factory::create();
}
return $this->faker;
}
}
@@ -0,0 +1,232 @@
<?php
namespace AppBundle\DataFixtures\External;
use AppBundle\DataFixtures\AbstractExternalFixture;
use CacheBundle\Comment\Manager\CommentManagerInterface;
use CacheBundle\Entity\Comment;
use Doctrine\ORM\EntityManagerInterface;
use IndexBundle\Index\External\InternalHoseIndex;
use IndexBundle\Index\IndexInterface;
use IndexBundle\Model\ArticleDocumentInterface;
use IndexBundle\Model\DocumentInterface;
use UserBundle\Entity\User;
/**
* Class ExternalFixture
* @package AppBundle\DataFixtures\External
*/
class ExternalFixture extends AbstractExternalFixture
{
/**
* Max documents in 'stage' environment
*/
const MAX = 300;
/**
* Load fixtures into index.
*
* @param IndexInterface $index A IndexInterface instance.
*
* @return void
*/
public function load(IndexInterface $index)
{
if ($this->checkEnvironment('prod')) {
return;
}
if (! $index instanceof InternalHoseIndex) {
throw new \LogicException(sprintf(
'External fixtures should be loaded into \'%s\' but \'%s\' given',
InternalHoseIndex::class,
get_class($index)
));
}
$patches = [
[
'sequence' => '1',
'title' => 'Al Kodmani Crime Family stole millions',
'lang' => 'en',
'geo_country' => 'US',
'geo_state' => 'Arizona',
'geo_city' => 'Amazing City',
'published' => date_create()->modify('- 10 days')->format('Y-m-d\TH:i:s\Z'),
'source_title' => 'CNN',
'duplicates_count' => 0,
'image_src' => 'http://lorempixel.com/120/100/',
'views' => 12012312,
],
[
'sequence' => '2',
'title' => 'Amazing cat',
'lang' => 'en',
'geo_country' => 'US',
'geo_state' => 'Maryland',
'published' => date_create()->modify('- 15 days')->format('Y-m-d\TH:i:s\Z'),
'source_title' => 'Asharq Al Awsat',
'duplicates_count' => 0,
'image_src' => '',
'views' => 112,
'section' => 'Lifestyle',
],
[
'sequence' => '3',
'title' => 'More about cats',
'lang' => 'ru',
'geo_country' => 'US',
'geo_state' => 'Louisiana',
'published' => date_create()->modify('- 10 days')->format('Y-m-d\TH:i:s\Z'),
'source_title' => 'AAAE',
'duplicates_count' => 0,
'image_src' => null,
'views' => 10001,
],
[
'sequence' => '4',
'title' => 'Cat and dog market',
'lang' => 'en',
'geo_country' => 'RU',
'published' => date_create()->modify('- 1 months')->format('Y-m-d\TH:i:s\Z'),
'source_title' => 'Aaj TV',
'duplicates_count' => 0,
'image_src' => 'http://lorempixel.com/120/100/',
'views' => 123,
],
[
'sequence' => '5',
'title' => 'Dogs are the best',
'lang' => 'en',
'published' => date_create()->modify('- 1 year')->format('Y-m-d\TH:i:s\Z'),
'source_title' => 'AACSB',
'duplicates_count' => 20,
'image_src' => 'http://lorempixel.com/120/100/',
'views' => 1123312,
],
[
'sequence' => '6',
'title' => 'Fish',
'lang' => 'af',
'geo_country' => 'US',
'geo_state' => 'Maryland',
'published' => date_create()->modify('- 25 days')->format('Y-m-d\TH:i:s\Z'),
'source_title' => 'Armenian Assembly of America',
'duplicates_count' => 0,
'image_src' => 'http://lorempixel.com/120/100/',
'views' => 100237312,
],
[
'sequence' => '7',
'title' => 'Cat and fish',
'lang' => 'en',
'geo_country' => 'US',
'geo_state' => 'Arizona',
'published' => date_create()->modify('- 3 days')->format('Y-m-d\TH:i:s\Z'),
'source_title' => '4A\'s',
'duplicates_count' => 10,
'image_src' => null,
'views' => 1543312,
'author_name' => 'Gracie Pfeffer',
'publisher' => 'msnbc',
],
[
'sequence' => '8',
'title' => 'Some',
'main' => 'Cat',
'lang' => 'af',
'geo_country' => 'US',
'geo_state' => 'Louisiana',
'published' => date_create()->modify('- 15 minutes')->format('Y-m-d\TH:i:s\Z'),
'source_title' => 'Asian American Press',
'duplicates_count' => 5,
'image_src' => '',
'views' => 10312,
],
[
'sequence' => '9',
'title' => 'Some',
'main' => 'Cat',
'lang' => 'af',
'geo_country' => 'US',
'geo_state' => 'Louisiana',
'published' => date_create()->modify('- 1 hours')->format('Y-m-d\TH:i:s\Z'),
'source_title' => 'CNN',
'duplicates_count' => 5,
'image_src' => '',
'views' => 10012,
],
];
/** @var ArticleDocumentInterface[] $documents */
$documents = [];
$max = $this->checkEnvironment('stage') ? self::MAX : count($patches);
foreach (range(0, $max) as $idx) {
$document = $this->generator->generate(10 + $idx);
if (isset($patches[$idx])) {
$document = $this->applyPatch($document, $patches[$idx]);
}
$documents[] = $document;
}
$index->index($documents);
//
// Some documents we should persist into our database in order to add
// comments for it.
//
/** @var EntityManagerInterface $em */
$em = $this->container->get('doctrine.orm.default_entity_manager');
$users = [
$em->getReference(User::class, 1),
$em->getReference(User::class, 2),
$em->getReference(User::class, 3),
];
$faker = $this->getFaker();
if (! $this->checkEnvironment('test')) {
foreach (range(0, $max, 5) as $idx) {
$commentsCount = random_int(15, 35);
$entity = $documents[$idx]->toDocumentEntity()
->setCommentsCount($commentsCount + 1);
$em->persist($entity);
foreach (range(0, $commentsCount) as $commentIdx) {
$comment = new Comment(
$faker->randomElement($users),
$faker->realText(),
$faker->boolean() ? 'Comment ' . $commentIdx : ''
);
$comment
->setCreatedAt(date_create()->modify('- ' . ($commentIdx + 1) . ' minutes'))
->setNew($commentIdx < CommentManagerInterface::NEW_COMMENT_POOL_SIZE)
->setDocument($entity);
$em->persist($comment);
}
}
}
$em->flush();
}
/**
* @param DocumentInterface $document A IndexDocumentInterface instance.
* @param array $path Array of patched properties with new values.
*
* @return DocumentInterface
*/
private function applyPatch(DocumentInterface $document, array $path)
{
return $document->mapRawData(function (array $data) use ($path) {
foreach ($path as $name => $value) {
$data[$name] = $value;
}
return $data;
});
}
}
@@ -0,0 +1,57 @@
<?php
namespace AppBundle\DataFixtures\Internal;
use AppBundle\DataFixtures\AbstractInternalIndexFixture;
use IndexBundle\Index\IndexInterface;
use IndexBundle\Index\Internal\InternalIndexInterface;
use IndexBundle\Model\Generator\InternalDocumentGenerator;
/**
* Class InternalFixture
* @package AppBundle\DataFixtures\Internal
*/
class InternalFixture extends AbstractInternalIndexFixture
{
/**
* @param IndexInterface $index A IndexInterface instance.
*
* @return void
*/
public function load(IndexInterface $index)
{
if (! $index instanceof InternalIndexInterface) {
throw new \LogicException(sprintf(
'External fixtures should be loaded into \'%s\' but \'%s\' given',
InternalIndexInterface::class,
get_class($index)
));
}
if (! $this->checkEnvironment([ 'dev', 'test' ])) {
return;
}
$documentManager = new InternalDocumentGenerator();
$documents = [];
for ($i = 0; $i < 100; ++$i) {
$document = $documentManager->generate();
$document['sequence'] = $i;
$document['title'] = 'About cat '.$i;
$documents[] = $document;
}
$index->index($documents);
}
/**
* Return index type for this fixture.
*
* @return string
*/
public function getIndex()
{
return self::INDEX_INTERNAL;
}
}
@@ -0,0 +1,98 @@
<?php
namespace AppBundle\DataFixtures\Source;
use AppBundle\DataFixtures\AbstractSourceIndexFixture;
use AppBundle\Manager\Source\SourceManagerInterface;
use CacheBundle\Entity\SourceList;
use IndexBundle\Index\IndexInterface;
use IndexBundle\Index\Source\SourceIndexInterface;
/**
* Class SourceFixture
* @package AppBundle\DataFixtures\Source
*/
class SourceFixture extends AbstractSourceIndexFixture
{
/**
* Maximum created sources.
*/
const MAX_COUNT = 100;
/**
* @param IndexInterface $index A IndexInterface instance.
*
* @return void
*/
public function load(IndexInterface $index)
{
if (! $index instanceof SourceIndexInterface) {
throw new \LogicException(sprintf(
'External fixtures should be loaded into \'%s\' but \'%s\' given',
SourceIndexInterface::class,
get_class($index)
));
}
if ($this->checkEnvironment('prod')) {
return;
}
// Wait to insure that all external documents was indexed.
sleep(5);
/** @var SourceManagerInterface $manager */
$manager = $this->container->get('app.source_manager');
$manager->pullFromExternal();
// Wait to insure that all sources was indexed.
sleep(5);
/** @var \Doctrine\ORM\EntityManagerInterface $em */
$em = $this->container->get('doctrine.orm.default_entity_manager');
$lists = $em->getRepository(SourceList::class)->findAll();
$response = $index->createRequestBuilder()->setLimit(1000)->build()->execute();
$min = (int) floor($response->getTotalCount() / 6);
$max = (int) floor($response->getTotalCount() / 2);
foreach ($lists as $list) {
$ids = $this->uniqueRandomIds($response->getDocuments(), mt_rand($min, $max));
$manager->addSourcesToList($ids, $list->getId());
// Wait to insure that all changes will be accepted and applied.
sleep(1);
}
}
/**
* Fetch unique random ids from sources.
*
* @param array $sources Source array.
* @param integer $count How much elements get.
*
* @return array
*/
private function uniqueRandomIds(array $sources, $count)
{
$alreadyFetched = [];
$fetchedCount = 0;
$sourceCount = count($sources);
$result = [];
while ($fetchedCount < $count) {
$idx = mt_rand(0, $sourceCount - 1);
if (in_array($idx, $alreadyFetched, true)) {
continue;
}
$alreadyFetched[] = $idx;
$fetchedCount++;
$result[] = $sources[$idx]['id'];
}
return $result;
}
}
@@ -0,0 +1,401 @@
<?php
namespace AppBundle\DataFixtures\ORM;
use AppBundle\DataFixtures\AbstractFixture;
use CacheBundle\Entity\Feed\QueryFeed;
use Doctrine\Common\Persistence\ObjectManager;
use UserBundle\Entity\Notification\Notification;
use UserBundle\Entity\Notification\NotificationTheme;
use UserBundle\Entity\Notification\Schedule\DailyNotificationSchedule;
use UserBundle\Entity\Notification\Schedule\MonthlyNotificationSchedule;
use UserBundle\Entity\Notification\Schedule\WeeklyNotificationSchedule;
use UserBundle\Entity\Recipient\AbstractRecipient;
use UserBundle\Entity\User;
use UserBundle\Enum\AppLimitEnum;
use UserBundle\Enum\NotificationTypeEnum;
use UserBundle\Enum\ThemeOptionExtractEnum;
use UserBundle\Enum\ThemeTypeEnum;
/**
* Class NotificationFixtures
* @package AppBundle\DataFixtures\ORM
*/
class NotificationFixtures extends AbstractFixture
{
private static $scheduleClasses = [
DailyNotificationSchedule::class,
WeeklyNotificationSchedule::class,
MonthlyNotificationSchedule::class,
];
private static $availableDiffMap = [
NotificationTypeEnum::ALERT => [
ThemeTypeEnum::PLAIN => [
'content.extract' => [
ThemeOptionExtractEnum::NO,
ThemeOptionExtractEnum::START,
ThemeOptionExtractEnum::CONTEXT,
],
'content.highlightKeywords.highlight' => [
true,
false,
],
'content.showInfo.sectionDivider' => [
true,
false,
],
'content.showInfo.sourceCountry' => [
true,
false,
],
'content.showInfo.userComments' => [
true,
false,
],
],
ThemeTypeEnum::ENHANCED => [
'content.extract' => [
ThemeOptionExtractEnum::NO,
ThemeOptionExtractEnum::START,
ThemeOptionExtractEnum::CONTEXT,
],
'content.highlightKeywords.highlight' => [
true,
false,
],
'content.showInfo.articleSentiment' => [
true,
false,
],
'content.showInfo.sourceCountry' => [
true,
false,
],
'content.showInfo.userComments' => [
true,
false,
],
],
],
];
/**
* Load data fixtures with the passed EntityManager
*
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*/
public function load(ObjectManager $manager)
{
switch (true) {
case $this->checkEnvironment('dev'):
$this->loadForDevelopment($manager);
break;
case $this->checkEnvironment('test'):
$this->loadForTesting($manager);
break;
}
}
/**
* Get the order of this fixture
*
* @return integer
*/
public function getOrder()
{
return 5;
}
/**
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*/
private function loadForDevelopment(ObjectManager $manager)
{
/** @var User $testUser */
$testUser = $this->getReference('test@email.com');
/** @var User $masterUser */
$masterUser = $this->getReference('master@email.com');
$feeds = [];
for ($i = 0; $this->hasReference('feed_'. $i); $i++) {
$feeds[] = $this->getReference('feed_'. $i);
}
$this->createNotifications($testUser, $feeds, $manager);
$this->createNotifications($masterUser, $feeds, $manager);
$manager->flush();
}
/**
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*/
private function loadForTesting(ObjectManager $manager)
{
/** @var User $testUser */
$testUser = $this->getReference('test@email.com');
/** @var User $masterUser */
$masterUser = $this->getReference('master@email.com');
/** @var NotificationTheme $theme */
$theme = $this->getReference('default_notification_theme');
/** @var QueryFeed $feedTest1 */
$feedTest1 = $this->getReference('feed_test1');
/** @var QueryFeed $feedTest3 */
$feedTest3 = $this->getReference('feed_test3');
$notification = Notification::create()
->setName('TestUser Notification1')
->setSubject('TestUser Notification1 Subject')
->setTimezone(new \DateTimeZone($this->getFaker()->timezone))
->setOwner($testUser)
->setBillingSubscription($testUser->getBillingSubscription())
->setTheme($theme)
->setNotificationType(NotificationTypeEnum::alert())
->setThemeType(ThemeTypeEnum::plain())
->setAutomatedSubject(false)
->setPublished()
->setActive()
->setAllowUnsubscribe(false)
->setUnsubscribeNotification(true)
->setSendWhenEmpty(false)
->addFeed($feedTest1)
->setSourcesCount(1);
$manager->persist($notification);
$notification = Notification::create()
->setName('MasterUser Notification1')
->setSubject('MasterUser Notification1 Subject')
->setTimezone(new \DateTimeZone($this->getFaker()->timezone))
->setOwner($masterUser)
->setBillingSubscription($testUser->getBillingSubscription())
->setTheme($theme)
->setNotificationType(NotificationTypeEnum::alert())
->setThemeType(ThemeTypeEnum::plain())
->setAutomatedSubject(false)
->setPublished()
->setActive()
->setAllowUnsubscribe(false)
->setUnsubscribeNotification(true)
->setSendWhenEmpty(false)
->addFeed($feedTest3)
->setSourcesCount(1);
$manager->persist($notification);
$manager->flush();
}
/**
* Create notification for specified user.
*
* @param User $user A User entity instance.
* @param array $feeds Array of feeds.
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*/
private function createNotifications(
User $user,
array $feeds,
ObjectManager $manager
) {
$userFeeds = \app\a\select(\nspl\op\methodCaller('isOwnedBy', [ $user ]), $feeds);
$userFeedsCount = count($userFeeds);
if ($userFeedsCount === 0) {
return;
}
$faker = $this->getFaker();
/** @var NotificationTheme $theme */
$theme = $this->getReference('default_notification_theme');
$allowedCount = ceil($user->getAllowedLimit(AppLimitEnum::alerts()) / 2);
$repository = $manager->getRepository(AbstractRecipient::class);
$recipients = $repository->findBy([ 'owner' => $user->getId() ]);
$recipientsCount = count($recipients);
for ($i = 0; $i < $allowedCount; ++$i) {
$feeds = $faker->randomElements($userFeeds, random_int(1, ceil($userFeedsCount / 2)));
$notificationType = NotificationTypeEnum::alert();
$themeType = new ThemeTypeEnum($faker->randomElement(ThemeTypeEnum::getAvailables()));
$count = random_int(0, $recipientsCount);
$notification = Notification::create()
->setName($faker->realText($faker->numberBetween(10, 15)))
->setSubject($faker->realText(50))
->setTimezone(new \DateTimeZone($this->getFaker()->timezone))
->setOwner($user)
->setBillingSubscription($user->getBillingSubscription())
->setTheme($theme)
->setNotificationType($notificationType)
->setThemeType($themeType)
->setAutomatedSubject($faker->boolean(35))
->setPublished($faker->boolean(45))
->setActive($faker->boolean(85))
->setAllowUnsubscribe($faker->boolean(85))
->setUnsubscribeNotification($faker->boolean(15))
->setSendWhenEmpty($faker->boolean(15));
if ($themeType->is(ThemeTypeEnum::plain())) {
$notification->setPlainThemeOptionsDiff(
$this->generateDiff($notificationType, $themeType)
);
}
for ($j = 0; $j < $count; ++$j) {
$notification->addRecipient($recipients[$j]);
}
if ($faker->boolean(60)) {
$notification->setSendUntil($faker->dateTimeBetween('+ 10 days', '+ 1 months'));
}
foreach ($feeds as $feed) {
$notification->addFeed($feed);
}
$notification->setSourcesCount(count($feeds));
$schedules = $this->generateSchedules($faker->numberBetween(1, 4));
foreach ($schedules as $schedule) {
$notification->addSchedule($schedule);
$manager->persist($schedule);
}
$user->useLimit(AppLimitEnum::alerts());
$manager->persist($notification);
$manager->persist($user);
}
}
/**
* @param NotificationTypeEnum $notificationType A NotificationTypeEnum instance.
* @param ThemeTypeEnum $themeType A ThemeTypeEnum instance.
*
* @return array
*/
private function generateDiff(
NotificationTypeEnum $notificationType,
ThemeTypeEnum $themeType
) {
$faker = $this->getFaker();
$available = self::$availableDiffMap[(string) $notificationType][(string) $themeType];
$availableCount = count($available);
/**
* @return \Generator
*/
$generatorFn = function () use ($available, $availableCount, $faker) {
$availableKeys = array_keys($available);
$used = [];
$usedCount = 0;
while ($availableCount > $usedCount) {
do {
$parameter = $faker->randomElement($availableKeys);
} while (in_array($parameter, $used, true));
$used[] = $parameter;
$usedCount++;
yield $parameter;
}
};
$parameters = $generatorFn();
$count = random_int(0, $availableCount - 1);
$diff = [];
for ($i = 0; $i < $count; ++$i) {
$parameter = $parameters->current();
$diff[$parameter] = $faker->randomElement($available[$parameter]);
$parameters->next();
}
return $diff;
}
/**
* Generate NotificationSchedule entity.
*
* @param integer $count How many schedules we need.
*
* @return \Generator
*/
private function generateSchedules($count)
{
$faker = $this->getFaker();
for ($i = 0; $i < $count; $i++) {
$class = $faker->randomElement(self::$scheduleClasses);
$methodName = 'create'. \app\c\getShortName($class);
yield $this->{$methodName}();
}
}
/**
* @return DailyNotificationSchedule
*
* Actually we call it.
* @SuppressWarnings(PHPMD.UnusedPrivateMethod)
*/
private function createDailyNotificationSchedule()
{
$faker = $this->getFaker();
return DailyNotificationSchedule::create()
->setDays($faker->randomElement(DailyNotificationSchedule::getAvailableDays()))
->setTime($faker->randomElement(DailyNotificationSchedule::getAvailableTime()));
}
/**
* @return WeeklyNotificationSchedule
*
* Actually we call it.
* @SuppressWarnings(PHPMD.UnusedPrivateMethod)
*/
private function createWeeklyNotificationSchedule()
{
$faker = $this->getFaker();
return WeeklyNotificationSchedule::create()
->setPeriod($faker->randomElement(WeeklyNotificationSchedule::getAvailablePeriod()))
->setDay($faker->randomElement(WeeklyNotificationSchedule::getAvailableDay()))
->setHour($faker->randomElement(range(0, 23)))
->setMinute($faker->randomElement(range(0, 55, 5)));
}
/**
* @return MonthlyNotificationSchedule
*
* Actually we call it.
* @SuppressWarnings(PHPMD.UnusedPrivateMethod)
*/
private function createMonthlyNotificationSchedule()
{
$faker = $this->getFaker();
return MonthlyNotificationSchedule::create()
->setDay($faker->randomElement(MonthlyNotificationSchedule::getAvailableDay()))
->setHour($faker->randomElement(range(0, 23)))
->setMinute($faker->randomElement(range(0, 55, 5)));
}
}
@@ -0,0 +1,77 @@
<?php
namespace AppBundle\DataFixtures\ORM;
use AppBundle\DataFixtures\AbstractFixture;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManagerInterface;
use UserBundle\Entity\Notification\Notification;
use UserBundle\Entity\Notification\NotificationSendHistory;
use UserBundle\Entity\Notification\Schedule\AbstractNotificationSchedule;
/**
* Class NotificationHistoryFixtures
* @package AppBundle\DataFixtures\ORM
*/
class NotificationHistoryFixtures extends AbstractFixture
{
/**
* Load data fixtures with the passed EntityManager
*
* @param ObjectManager|EntityManagerInterface $manager A ObjectManager
* instance.
*
* @return void
*/
public function load(ObjectManager $manager)
{
if (! $this->checkEnvironment('dev')) {
return;
}
$notifications = $manager->getRepository(Notification::class)
->createQueryBuilder('Notification')
->select('Notification, Schedule')
->join('Notification.schedules', 'Schedule')
->getQuery()
->getResult();
$faker = $this->getFaker();
/** @var Notification $notification */
foreach ($notifications as $notification) {
$max = random_int(15, 40);
for ($i = 0; $i < $max; ++$i) {
$schedule = $notification->getSchedules()->toArray();
$historySchedule = array_map(function (AbstractNotificationSchedule $schedule) {
$historySchedule = clone $schedule;
$historySchedule->setNotification(null);
return $historySchedule;
}, $faker->randomElements($schedule, random_int(1, count($schedule))));
$history = new NotificationSendHistory(
$notification,
$historySchedule
);
$history->setDate($faker->dateTimeBetween('- 1 year'));
$manager->persist($history);
}
$manager->flush();
}
}
/**
* Get the order of this fixture
*
* @return integer
*/
public function getOrder()
{
return 6;
}
}
@@ -0,0 +1,43 @@
<?php
namespace AppBundle\DataFixtures\ORM;
use AppBundle\DataFixtures\AbstractFixture;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManagerInterface;
use UserBundle\Entity\Notification\NotificationTheme;
use UserBundle\Entity\Notification\NotificationThemeOptions;
/**
* Class NotificationThemeFixtures
* @package AppBundle\DataFixtures\ORM
*/
class NotificationThemeFixtures extends AbstractFixture
{
/**
* Load data fixtures with the passed EntityManager
*
* @param ObjectManager|EntityManagerInterface $manager A ObjectManager instance.
*
* @return void
*/
public function load(ObjectManager $manager)
{
$defaultOptions = NotificationThemeOptions::createDefault();
//
// Create default theme which should not be editable by master users.
//
$default = NotificationTheme::create()
->setName('Socialhose theme')
->setEnhanced($defaultOptions)
->setPlain($defaultOptions)
->setDefault(true);
$this->addReference('default_notification_theme', $default);
$manager->persist($default);
$manager->flush();
}
}
@@ -0,0 +1,38 @@
<?php
namespace AppBundle\DataFixtures\ORM;
use AppBundle\DataFixtures\AbstractFixture;
use Doctrine\Common\Persistence\ObjectManager;
use UserBundle\Entity\Organization;
/**
* Class OrganizationFixture
* @package AppBundle\DataFixtures\ORM
*/
class OrganizationFixture extends AbstractFixture
{
/**
* Load data fixtures with the passed EntityManager
*
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function load(ObjectManager $manager)
{
if ($this->checkEnvironment('prod')) {
return;
}
$organization = Organization::create()
->setName('Test Organization');
$this->setReference('organization', $organization);
$manager->persist($organization);
$manager->flush();
}
}
@@ -0,0 +1,63 @@
<?php
namespace AppBundle\DataFixtures\ORM;
use AppBundle\DataFixtures\AbstractFixture;
use Doctrine\Common\Persistence\ObjectManager;
use PaymentBundle\Entity\Model\Money;
use PaymentBundle\Entity\Payment;
use PaymentBundle\Enum\PaymentGatewayEnum;
use PaymentBundle\Enum\PaymentStatusEnum;
use UserBundle\Entity\Subscription\AbstractSubscription;
/**
* Class PaymentFixture
* @package AppBundle\DataFixtures\ORM
*/
class PaymentFixture extends AbstractFixture
{
/**
* Load data fixtures with the passed EntityManager
*
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*/
public function load(ObjectManager $manager)
{
if (! $this->checkEnvironment('dev')) {
return;
}
/** @var AbstractSubscription[] $subscriptions */
$subscriptions = [
$this->getReference('first_subscription'),
$this->getReference('second_subscription'),
$this->getReference('personal_subscription'),
];
$faker = $this->getFaker();
for ($i = 0; $i < 50; $i++) {
$payment = Payment::create()
->setGateway(PaymentGatewayEnum::paypal())
->setAmount(new Money($faker->randomFloat(2, 10, 20), 'USD'))
->setStatus(new PaymentStatusEnum($faker->randomElement(PaymentStatusEnum::getAvailables())))
->setSubscription($faker->randomElement($subscriptions))
->setTransactionId($faker->md5);
$manager->persist($payment);
}
$manager->flush();
}
/**
* Get the order of this fixture.
*
* @return integer
*/
public function getOrder()
{
return 3;
}
}
@@ -0,0 +1,111 @@
<?php
namespace AppBundle\DataFixtures\ORM;
use AppBundle\DataFixtures\AbstractFixture;
use Doctrine\Common\Persistence\ObjectManager;
use UserBundle\Entity\Plan;
/**
* Class PlanFixture
* @package AppBundle\DataFixtures\ORM
*/
class PlanFixture extends AbstractFixture
{
/**
* Load data fixtures with the passed EntityManager
*
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*/
public function load(ObjectManager $manager)
{
$plan = Plan::create()
->setTitle('Social Starter')
->setInnerName('social_starter')
->setSearchesPerDay(500)
->setSavedFeeds(15)
->setMasterAccounts(1)
->setSubscriberAccounts(5)
->setAlerts(5)
->setNewsletters(1)
->setAnalytics(false)
->setNews(true)
->setBlog(false)
->setReddit(false)
->setInstagram(true)
->setIsDefault(true)
->setTwitter(false)
->setPrice(160.0);
$manager->persist($plan);
$this->setReference('starter_plan', $plan);
$plan = Plan::create()
->setTitle('Pr Starter')
->setInnerName('pr_starter')
->setSearchesPerDay(1000)
->setSavedFeeds(20)
->setMasterAccounts(1)
->setSubscriberAccounts(20)
->setAlerts(20)
->setNewsletters(10)
->setAnalytics(true)
->setNews(true)
->setBlog(false)
->setIsDefault(true)
->setReddit(false)
->setInstagram(true)
->setTwitter(false)
->setPrice(190.0);
$manager->persist($plan);
$this->setReference('pr_starter', $plan);
$plan = Plan::create()
->setTitle('The Works')
->setInnerName('the_works')
->setSearchesPerDay(2000)
->setSavedFeeds(25)
->setMasterAccounts(1)
->setSubscriberAccounts(25)
->setAlerts(30)
->setNewsletters(20)
->setAnalytics(true)
->setNews(true)
->setBlog(true)
->setReddit(true)
->setInstagram(true)
->setIsDefault(true)
->setTwitter(true)
->setPrice(325.0);
$manager->persist($plan);
$this->setReference('the_works', $plan);
$plan = Plan::create()
->setTitle('Free')
->setInnerName('free')
->setSearchesPerDay(100)
->setSavedFeeds(0)
->setMasterAccounts(1)
->setSubscriberAccounts(0)
->setAlerts(5)
->setNewsletters(5)
->setAnalytics(true)
->setNews(true)
->setBlog(true)
->setReddit(true)
->setInstagram(true)
->setTwitter(true)
->setIsDefault(true)
->setPrice(0.0);
$manager->persist($plan);
$this->setReference('free', $plan);
$manager->flush();
}
}
@@ -0,0 +1,198 @@
<?php
namespace AppBundle\DataFixtures\ORM;
use AppBundle\DataFixtures\AbstractFixture;
use AppBundle\Exception\LimitExceedException;
use CacheBundle\Entity\Feed\QueryFeed;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Entity\Query\StoredQuery;
use CacheBundle\Entity\Category;
use Common\Enum\FieldNameEnum;
use Common\Enum\PublisherTypeEnum;
use Doctrine\Common\Persistence\ObjectManager;
use UserBundle\Entity\User;
use UserBundle\Enum\AppLimitEnum;
/**
* Class QueryFeedFixture
* @package AppBundle\DataFixtures\ORM
*/
class QueryFeedFixture extends AbstractFixture
{
/**
* Max available feeds.
*/
const MAX_FEEDS = 5;
/**
* Load data fixtures with the passed EntityManager
*
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function load(ObjectManager $manager)
{
switch (true) {
case $this->checkEnvironment('dev'):
$this->loadForDevelopment($manager);
break;
case $this->checkEnvironment('test'):
$this->loadForTesting($manager);
break;
}
}
/**
* Get the order of this fixture.
*
* @return integer
*/
public function getOrder()
{
return 3;
}
/**
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*/
private function loadForDevelopment(ObjectManager $manager)
{
/** @var User[] $users */
$users = [
$this->getReference('test@email.com'),
$this->getReference('master@email.com'),
];
for ($i = 0; $i < self::MAX_FEEDS; $i++) {
$raw = strtolower($this->getFaker()->word);
$index = random_int(0, count($users) - 1);
$user = $users[$index];
try {
$user->useLimit(AppLimitEnum::feeds());
} catch (LimitExceedException $exception) {
continue;
}
$categoriesCount = count($user->getCategories());
$query = $this->createQuery($raw);
$category = $this->getCategory($user, $categoriesCount);
$feed = $this->createQueryFeed($query, 'test'. $raw, $user, $category);
$manager->persist($query);
$manager->persist($feed);
$manager->persist($user);
$this->addReference('feed_'. $i, $feed);
}
$manager->flush();
}
/**
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*/
private function loadForTesting(ObjectManager $manager)
{
/** @var User $testUser */
$testUser = $this->getReference('test@email.com');
/** @var User $masterUser */
$masterUser = $this->getReference('master@email.com');
$testUser->useLimit(AppLimitEnum::feeds(), 2);
$query = $this->createQuery('test1');
$feed = $this->createQueryFeed($query, 'test1', $testUser, $testUser->getCategories()->first());
$manager->persist($query);
$manager->persist($feed);
$this->addReference('feed_test1', $feed);
$query = $this->createQuery('test2');
$feed = $this->createQueryFeed($query, 'test2', $testUser, $testUser->getCategories()->first());
$manager->persist($query);
$manager->persist($feed);
$this->addReference('feed_test2', $feed);
$masterUser->useLimit(AppLimitEnum::feeds());
$query = $this->createQuery('test3');
$feed = $this->createQueryFeed($query, 'test3', $masterUser, $masterUser->getCategories()->first());
$manager->persist($query);
$manager->persist($feed);
$this->addReference('feed_test3', $feed);
$manager->persist($testUser);
$manager->persist($masterUser);
$manager->flush();
}
/**
* @param User $user A User entity instance.
* @param integer $count Total count of categories.
*
* @return \CacheBundle\Entity\Category
*/
private function getCategory(User $user, $count)
{
static $i = 0;
if ($i >= $count) {
$i = 0;
}
return $user->getCategories()[$i++];
}
/**
* @param string $searchString That keyword is used for searching.
*
* @return StoredQuery
*/
private function createQuery($searchString)
{
return StoredQuery::create()
->setRaw($searchString)
->setFields([
FieldNameEnum::TITLE,
FieldNameEnum::MAIN,
])
->setTotalCount($this->getFaker()->randomNumber())
->setDate(date_create())
->setNormalized($searchString)
->setHash($this->getFaker()->md5);
}
/**
* @param StoredQuery $query A StoredQuery instance.
* @param string $name Feed name.
* @param User $user Feed owner.
* @param Category $category In which category place feed.
*
* @return AbstractFeed
*/
private function createQueryFeed(StoredQuery $query, $name, User $user, Category $category)
{
return QueryFeed::create()
->setPublisherTypes($this->getFaker()
->randomElements(PublisherTypeEnum::getAvailables(), random_int(1, 2)))
->setQuery($query)
->setCategory($category)
->setName($name)
->setUser($user);
}
}
@@ -0,0 +1,90 @@
<?php
namespace AppBundle\DataFixtures\ORM;
use AppBundle\DataFixtures\AbstractFixture;
use Doctrine\Common\Persistence\ObjectManager;
use UserBundle\Entity\Recipient\GroupRecipient;
use UserBundle\Entity\Recipient\PersonRecipient;
use UserBundle\Entity\User;
use UserBundle\Enum\UserRoleEnum;
/**
* Class RecipientFixture
* @package AppBundle\DataFixtures\ORM
*/
class RecipientFixture extends AbstractFixture
{
/**
* Load data fixtures with the passed EntityManager
*
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function load(ObjectManager $manager)
{
if (! $this->checkEnvironment('dev')) {
return;
}
/** @var User[] $users */
$users = [
$this->getReference('test@email.com'),
$this->getReference('master@email.com'),
];
$faker = $this->getFaker();
foreach ($users as $user) {
if ($user->hasRole(UserRoleEnum::MASTER_USER)) {
$personCount = random_int(4, 10);
$groupCount = random_int(3, 5);
$persons = [];
for ($i = 0; $i < $personCount; ++$i) {
$person = PersonRecipient::create()
->setFirstName($faker->firstName)
->setLastName($faker->lastName)
->setEmail($faker->email)
->setOwner($user);
$persons[] = $person;
$manager->persist($person);
}
for ($i = 0; $i < $groupCount; ++$i) {
$groupPersons = $faker->randomElements($persons, random_int(2, 4));
$group = GroupRecipient::create()
->setName($faker->word)
->setDescription($faker->realText())
->setOwner($user);
foreach ($groupPersons as $person) {
$group->addRecipient($person);
}
$manager->persist($group);
}
$manager->persist($user);
$manager->flush();
}
}
}
/**
* Get the order of this fixture.
*
* @return integer
*/
public function getOrder()
{
return 3;
}
}
@@ -0,0 +1,40 @@
<?php
namespace AppBundle\DataFixtures\ORM;
use AppBundle\DataFixtures\AbstractFixture;
use Doctrine\Common\Persistence\ObjectManager;
use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken;
/**
* Class RefreshTokenFixture
* @package AppBundle\DataFixtures\ORM
*/
class RefreshTokenFixture extends AbstractFixture
{
/**
* Load data fixtures with the passed EntityManager
*
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function load(ObjectManager $manager)
{
if (! $this->checkEnvironment('test')) {
return;
}
$token = new RefreshToken();
$token
->setRefreshToken('user1_token')
->setUsername('test@email.com')
->setValid(date_create()->modify('+ 10 days'));
$manager->persist($token);
$manager->flush();
}
}
@@ -0,0 +1,30 @@
<?php
namespace AppBundle\DataFixtures\ORM;
use AppBundle\AppBundleServices;
use AppBundle\DataFixtures\AbstractFixture;
use Doctrine\Common\Persistence\ObjectManager;
/**
* Class SiteConfigurationFixture
* @package AppBundle\DataFixtures\ORM
*/
class SiteConfigurationFixture extends AbstractFixture
{
/**
* Load data fixtures with the passed EntityManager
*
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function load(ObjectManager $manager)
{
$this->container->get(AppBundleServices::CONFIGURATION)
->syncWithDefinitions();
}
}
@@ -0,0 +1,62 @@
<?php
namespace AppBundle\DataFixtures\ORM;
use AppBundle\DataFixtures\AbstractFixture;
use CacheBundle\Entity\SourceList;
use Doctrine\Common\Persistence\ObjectManager;
/**
* Class SourceListFixture
* @package AppBundle\DataFixtures\ORM
*/
class SourceListFixture extends AbstractFixture
{
/**
* Load data fixtures with the passed EntityManager
*
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*/
public function load(ObjectManager $manager)
{
if (! $this->checkEnvironment('dev')) {
return;
}
$users = [
$this->getReference('test@email.com'),
$this->getReference('master@email.com'),
];
for ($i = 1; $i <= 25; $i++) {
$user = $users[$i % 2];
$sourceList = new SourceList();
$sourceList->setName('Source list '. $i);
$sourceList->setUser($user);
if ($this->getFaker()->boolean()) {
$sourceList
->setUpdatedBy($user)
->setUpdatedAt(new \DateTime());
}
$manager->persist($sourceList);
}
$manager->flush();
}
/**
* Get the order of this fixture
*
* @return integer
*/
public function getOrder()
{
return 5;
}
}
@@ -0,0 +1,237 @@
<?php
namespace AppBundle\DataFixtures\ORM;
use AppBundle\DataFixtures\AbstractFixture;
use CacheBundle\Entity\Category;
use Doctrine\Common\Persistence\ObjectManager;
use FOS\UserBundle\Model\UserManagerInterface;
use PaymentBundle\Enum\PaymentGatewayEnum;
use UserBundle\Entity\Organization;
use UserBundle\Entity\Plan;
use UserBundle\Entity\Subscription\OrganizationSubscription;
use UserBundle\Entity\Subscription\PersonalSubscription;
use UserBundle\Entity\User;
use UserBundle\Enum\UserRoleEnum;
/**
* Class UserFixture
* @package AppBundle\DataFixtures\ORM
*/
class UserFixture extends AbstractFixture
{
/**
* Load data fixtures with the passed EntityManager
*
* @param ObjectManager $manager A ObjectManager instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function load(ObjectManager $manager)
{
/** @var UserManagerInterface $userManager */
$userManager = $this->container->get('fos_user.user_manager');
if ($this->checkEnvironment('prod')) {
/** @var User $superAdmin */
$superAdmin = $userManager->createUser();
$superAdmin
->setFirstName('Super')
->setLastName('Admin')
->setEmail('super_admin@socialhose.io')
->setPhoneNumber('44444444444')
->setVerified()
->setEnabled(true)
->addRole(UserRoleEnum::SUPER_ADMIN)
->setPlainPassword('FEvJNcKGk2rVDMVL');
$userManager->updateUser($superAdmin);
$this->setReference('super_admin@socialhose.io', $superAdmin);
return;
}
//
// Create organization subscription and users.
//
/** @var Organization $organization */
$organization = $this->getReference('organization');
/** @var Plan $businessPlan */
$businessPlan = $this->getReference('starter_plan');
/** @var Plan $basicPlan */
$basicPlan = $this->getReference('pr_starter');
//
// First department.
//
$firstSubscription = OrganizationSubscription::create()
->setGateway(PaymentGatewayEnum::paypal())
->setPayed(true)
->setPlan($businessPlan)
->setOrganization($organization)
->setOrganizationAddress('First department address')
->setOrganizationEmail('first_department.organization@email.com')
->setOrganizationPhone('111111111');
/** @var User $master */
$master = $userManager->createUser();
$master
->setFirstName('John')
->setLastName('Smith')
->setEmail('test@email.com')
->setPhoneNumber('11111111111')
->setVerified()
->setEnabled(true)
->setBillingSubscription($firstSubscription)
->addRole(UserRoleEnum::MASTER_USER)
->setPlainPassword('test');
$main = Category::createMainCategory($master);
Category::createSharedCategory($master);
Category::createTrashCategory($master);
$subMain = Category::createChild($main, $master, 'Sub main');
Category::createChild($subMain, $master, 'Sub main sub 1');
Category::createChild($subMain, $master, 'Sub main sub 2');
$subMainSub3 = Category::createChild($subMain, $master, 'Sub main sub 3');
Category::createChild($subMainSub3, $master, 'Test');
$firstSubscription->setOwner($master);
$userManager->updateUser($master);
$this->setReference('test@email.com', $master);
$this->setReference('first_subscription', $firstSubscription);
/** @var User $user */
$user = $userManager->createUser();
$user
->setFirstName('John')
->setLastName('Smith')
->setEmail('test_subscriber@email.com')
->setPhoneNumber('11111111112')
->setVerified()
->setEnabled(true)
->setBillingSubscription($firstSubscription)
->addRole(UserRoleEnum::SUBSCRIBER)
->setMasterUser($master)
->setPlainPassword('test');
$manager->persist($firstSubscription);
$userManager->updateUser($user);
$this->setReference('test_subscriber@email.com', $user);
//
// Second department.
//
$secondSubscription = OrganizationSubscription::create()
->setGateway(PaymentGatewayEnum::paypal())
->setPayed(true)
->setPlan($basicPlan)
->setOrganization($organization)
->setOrganizationAddress('Second department address')
->setOrganizationEmail('second_department.organization@email.com')
->setOrganizationPhone('222222222');
/** @var User $user */
$user = $userManager->createUser();
$user
->setFirstName('Master')
->setLastName('Smith')
->setEmail('master@email.com')
->setPhoneNumber('22222222222')
->setVerified()
->setEnabled(true)
->setBillingSubscription($secondSubscription)
->addRole(UserRoleEnum::MASTER_USER)
->setPlainPassword('test');
$main = Category::createMainCategory($user);
Category::createSharedCategory($user);
Category::createTrashCategory($user);
Category::createChild($main, $user, 'Sub main');
$secondSubscription->setOwner($user);
$manager->persist($secondSubscription);
$userManager->updateUser($user);
$this->setReference('master@email.com', $user);
$this->setReference('second_subscription', $secondSubscription);
//
// Individual subscription.
//
$personSubscription = PersonalSubscription::create()
->setGateway(PaymentGatewayEnum::paypal())
->setPayed(true)
->setPlan($basicPlan);
$user = $userManager->createUser();
$user
->setFirstName('Jane')
->setLastName('Smith')
->setEmail('jane@person.com')
->setPhoneNumber('33333333333')
->setVerified()
->setEnabled(true)
->setBillingSubscription($personSubscription)
->addRole(UserRoleEnum::MASTER_USER)
->setPlainPassword('test');
Category::createMainCategory($user);
Category::createTrashCategory($user);
$userManager->updateUser($user);
$this->setReference('jane@person.com', $user);
$this->setReference('personal_subscription', $personSubscription);
$personSubscription->setOwner($user);
$manager->persist($personSubscription);
//
// Admins.
//
/** @var User $superAdmin */
$superAdmin = $userManager->createUser();
$superAdmin
->setFirstName('Super')
->setLastName('Admin')
->setEmail('super_admin@socialhose.com')
->setPhoneNumber('44444444444')
->setVerified()
->setEnabled(true)
->addRole(UserRoleEnum::SUPER_ADMIN)
->setPlainPassword('test');
$userManager->updateUser($superAdmin);
$this->setReference('super_admin@socialhose.com', $superAdmin);
/** @var User $admin */
$admin = $userManager->createUser();
$admin
->setFirstName('Just')
->setLastName('Admin')
->setEmail('admin@socialhose.com')
->setPhoneNumber('55555555555')
->setVerified()
->setEnabled(true)
->addRole(UserRoleEnum::ADMIN)
->setPlainPassword('test');
$userManager->updateUser($admin);
$this->setReference('admin@socialhose.com', $admin);
}
/**
* Get the order of this fixture.
*
* @return integer
*/
public function getOrder()
{
return 2;
}
}
@@ -0,0 +1,48 @@
<?php
namespace AppBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
/**
* Class AppExtension
* @package ApiDocBundle\DependencyInjection
*/
class AppExtension extends Extension
{
/**
* Loads a specific configuration.
*
* @param array $configs An array of configuration values.
* @param ContainerBuilder $container A ContainerBuilder instance.
*
* @throws \InvalidArgumentException When provided tag is not defined in
* this extension.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$environment = $container->getParameter('kernel.environment');
if ($environment === 'prod') {
//
// Inject NelmioApiDocBundle form extensions 'cause we don't load full
// nelmio configuration on production and we got error.
//
$loader->load('nelmio_form.yml');
}
$loader->load('services.yml');
}
}
@@ -0,0 +1,99 @@
<?php
namespace AppBundle\Doctrine\DBAL\Types;
use AppBundle\Enum\AbstractEnum;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
/**
* Class AbstractEnumType
* @package AppBundle\Doctrine\DBAL\Types
*/
abstract class AbstractEnumType extends Type
{
/**
* Gets the SQL declaration snippet for a field of this type.
*
* @param array $fieldDeclaration The field declaration.
* @param AbstractPlatform $platform The currently used database
* platform.
*
* @return string
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getVarcharTypeDeclarationSQL($fieldDeclaration);
}
/**
* Converts a value from its PHP representation to its database
* representation of this type.
*
* @param mixed $value The value to convert.
* @param AbstractPlatform $platform The currently used database platform.
*
* @return mixed The database representation of the value.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if (is_string($value)) {
$class = $this->getClass();
$value = new $class($value);
}
if (! $value instanceof AbstractEnum) {
throw new \InvalidArgumentException(
'Invalid value, must be instance of '. AbstractEnum::class
.' but '. (is_object($value) ? get_class($value) : gettype($value))
.' given'
);
}
return $value->getValue();
}
/**
* Return concrete enum class
*
* @return string
*/
abstract protected function getClass();
/**
* Converts a value from its database representation to its PHP
* representation of this type.
*
* @param mixed $value The value to convert.
* @param AbstractPlatform $platform The currently used database platform.
*
* @return mixed The PHP representation of the value.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
$class = $this->getClass();
if ($value !== null) {
$value = new $class($value);
}
return $value;
}
/**
* Gets the default length of this type.
*
* @param AbstractPlatform $platform The currently used database platform.
*
* @return integer|null
*/
public function getDefaultLength(AbstractPlatform $platform)
{
return $platform->getVarcharDefaultLength();
}
}
@@ -0,0 +1,96 @@
<?php
namespace AppBundle\Doctrine\DBAL\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
/**
* Class DateTimeZoneType
* @package AppBundle\Doctrine\DBAL\Types
*/
class DateTimeZoneType extends Type
{
/**
* Gets the name of this type.
*
* @return string
*/
public function getName()
{
return 'datetimezone';
}
/**
* Gets the SQL declaration snippet for a field of this type.
*
* @param array $fieldDeclaration The field declaration.
* @param AbstractPlatform $platform The currently used database
* platform.
*
* @return string
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getVarcharTypeDeclarationSQL($fieldDeclaration);
}
/**
* Gets the default length of this type.
*
* @param AbstractPlatform $platform The currently used database platform.
*
* @return integer|null
*/
public function getDefaultLength(AbstractPlatform $platform)
{
return $platform->getVarcharDefaultLength();
}
/**
* Converts a value from its PHP representation to its database
* representation of this type.
*
* @param mixed $value The value to convert.
* @param AbstractPlatform $platform The currently used database platform.
*
* @return mixed The database representation of the value.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if (! $value instanceof \DateTimeZone) {
throw new \InvalidArgumentException(
'Expects \DateTimeZone, but got '. gettype($value)
);
}
return $value->getName();
}
/**
* Converts a value from its database representation to its PHP
* representation of this type.
*
* @param mixed $value The value to convert.
* @param AbstractPlatform $platform The currently used database platform.
*
* @return mixed The PHP representation of the value.
*
* @throws ConversionException If can't convert from database to php value.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
$val = new \DateTimeZone($value);
if (! $val instanceof \DateTimeZone) {
throw ConversionException::conversionFailed($value, $this->getName());
}
return $val;
}
}
@@ -0,0 +1,42 @@
<?php
namespace AppBundle\Doctrine\ORM;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\EntityRepository;
/**
* Class BaseEntityRepository
*
* @package AppBundle\Doctrine\ORM
*/
class BaseEntityRepository extends EntityRepository
{
/**
* Persist entity.
*
* @param object $entity A persisted entity. Should be have same class which
* is used to create repository.
* @param boolean $flush Immediately flush entity change or not.
*
* @return void
*/
public function persist($entity, $flush = true)
{
if (! ClassUtils::getRealClass(get_class($entity)) === $this->_entityName) {
throw new \InvalidArgumentException(sprintf(
'%s: \'$entity\' should be instance of \'%s\' but \'%s\' given',
__CLASS__,
$this->_entityName,
\app\op\getPrintableType($entity)
));
}
$this->_em->persist($entity);
if ($flush) {
$this->_em->flush($entity);
}
}
}
@@ -0,0 +1,42 @@
<?php
namespace AppBundle\Entity;
/**
* Class ActivateAwareEntityTrait
* @package AppBundle\Entity
*/
trait ActivateAwareEntityTrait
{
/**
* @var boolean
*
* @ORM\Column(type="boolean")
*/
protected $active = true;
/**
* Set active
*
* @param boolean $active Flag, notification will be render if set.
*
* @return static
*/
public function setActive($active = true)
{
$this->active = $active;
return $this;
}
/**
* Get active
*
* @return boolean
*/
public function isActive()
{
return $this->active;
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace AppBundle\Entity;
/**
* Trait AbstractEntity
*
* Base entity trait used for implementing methods from EntityInterface and some
* standard mapping.
*
* @package AppBundle\Entity
*/
trait BaseEntityTrait
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Create entity instance for fluid interface access.
*
* @return static
*/
public static function create()
{
return new static();
}
/**
* Get entity type
*
* @return string
*/
public function getEntityType()
{
return \app\op\camelCaseToUnderscore(\app\c\getShortName(static::class));
}
}
+257
View File
@@ -0,0 +1,257 @@
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Psr\Cache\CacheItemInterface;
/**
* Class CacheItem
*
* @ORM\Table(name="cache_items")
* @ORM\Entity
*
* @package AppBundle\Entity
*/
class CacheItem implements CacheItemInterface
{
/**
* @var string
*
* @ORM\Column(name="`key`")
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
*/
private $key;
/**
* @var mixed
*
* @ORM\Column(type="json_array")
*/
private $value;
/**
* @var integer
*
* @ORM\Column(type="integer")
*/
private $lifetime;
/**
* @var integer
*
* @ORM\Column(type="bigint")
*/
private $expiresAt;
/**
* @var boolean
*/
private $isHit = true;
/**
* CacheItem constructor.
*
* @param string $key Cache item key.
* @param mixed $value Cached value.
* @param integer $lifetime Value lifetime in seconds.
* @param boolean $isHit Is item fetched.
*/
public function __construct(
$key = null,
$value = null,
$lifetime = null,
$isHit = true
) {
$this->key = $key;
$this->value = $value;
$this->lifetime = $lifetime;
$this->expiresAt = time() + $lifetime;
$this->isHit = $isHit;
}
/**
* @return string
*/
public function getKey()
{
return $this->key;
}
/**
* @param string $key Item key.
*
* @return CacheItem
*/
public function setKey($key)
{
$this->key = $key;
return $this;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* @param mixed $value A cached value.
*
* @return CacheItem
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
/**
* Retrieves the value of the item from the cache associated with this
* object's key.
*
* The value returned must be identical to the value originally stored by
* set().
*
* If isHit() returns false, this method MUST return null. Note that null
* is a legitimate cached value, so the isHit() method SHOULD be used to
* differentiate between "null value was found" and "no value was found."
*
* @return mixed
* The value corresponding to this cache item's key, or null if not
* found.
*/
public function get()
{
return $this->value;
}
/**
* Sets the value represented by this cache item.
*
* The $value argument may be any item that can be serialized by PHP,
* although the method of serialization is left up to the Implementing
* Library.
*
* @param mixed $value The value to be stored.
*
* @return static
* The invoked object.
*/
public function set($value)
{
$this->value = $value;
return $this;
}
/**
* Confirms if the cache item lookup resulted in a cache hit.
*
* Note: This method MUST NOT have a race condition between calling isHit()
* and calling get().
*
* @return boolean
* True if the request resulted in a cache hit. False otherwise.
*/
public function isHit()
{
return $this->isHit;
}
/**
* @return integer
*/
public function getLifetime()
{
return $this->lifetime;
}
/**
* @param integer $lifetime How long this item is valid in seconds.
*
* @return CacheItem
*/
public function setLifetime($lifetime)
{
$this->lifetime = $lifetime;
return $this;
}
/**
* @return integer
*/
public function getExpiresAt()
{
return $this->expiresAt;
}
/**
* Sets the expiration time for this cache item.
*
* @param \DateTimeInterface|null $expiration The point in time after which
* the item MUST be considered expired.
* If null is passed explicitly,
* a default value MAY be used.
* If none is set, the value should
* be stored permanently or for as
* long as the implementation allows.
*
* @return static
*/
public function expiresAt($expiration)
{
if (null === $expiration) {
$this->expiresAt = $this->lifetime > 0 ? time() + $this->lifetime : null;
} elseif ($expiration instanceof \DateTimeInterface) {
$this->expiresAt = (int) $expiration->format('U');
} else {
throw new \InvalidArgumentException(sprintf(
'Expiration date must implement DateTimeInterface or be null, "%s" given',
is_object($expiration) ? get_class($expiration) : gettype($expiration)
));
}
return $this;
}
/**
* Sets the expiration time for this cache item.
*
* @param integer|\DateInterval|null $time The period of time from the present
* after which the item MUST be considered
* expired. An integer parameter is
* understood to be the time in seconds
* until expiration. If null is passed
* explicitly, a default value MAY be
* used. If none is set, the value
* should be stored permanently or for
* as long as the implementation allows.
*
* @return static
*/
public function expiresAfter($time)
{
if (null === $time) {
$this->expiresAt = $this->lifetime > 0 ? time() + $this->lifetime : null;
} elseif ($time instanceof \DateInterval) {
$this->expiresAt = (int) \DateTime::createFromFormat('U', time())->add($time)->format('U');
} elseif (is_int($time)) {
$this->expiresAt = $time + time();
} else {
throw new \InvalidArgumentException(sprintf(
'Expiration date must be an integer, a DateInterval or null, "%s" given',
is_object($time) ? get_class($time) : gettype($time)
));
}
return $this;
}
}
+148
View File
@@ -0,0 +1,148 @@
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* EmailedDocument
*
* @ORM\Table(name="emailed_documents")
* @ORM\Entity
*/
class EmailedDocument implements EntityInterface
{
use BaseEntityTrait;
/**
* @var string[]
*
* @ORM\Column(type="array")
*
* @Assert\Count(min=1)
*/
private $emailTo;
/**
* @var string
*
* @ORM\Column
*
* @Assert\NotBlank
*/
private $emailReplyTo;
/**
* @var string
*
* @ORM\Column(nullable=true)
*/
private $subject;
/**
* @var string
*
* @ORM\Column(type="text")
*
* @Assert\NotBlank
*/
private $content;
/**
* Set emailTo
*
* @param array|mixed $emailTo List of recipients.
*
* @return EmailedDocument
*/
public function setEmailTo($emailTo)
{
$this->emailTo = (array) $emailTo;
return $this;
}
/**
* Get emailTo
*
* @return array
*/
public function getEmailTo()
{
return $this->emailTo;
}
/**
* Set emailReplyTo
*
* @param string $emailReplyTo Reply to.
*
* @return EmailedDocument
*/
public function setEmailReplyTo($emailReplyTo)
{
$this->emailReplyTo = $emailReplyTo;
return $this;
}
/**
* Get emailReplyTo
*
* @return string
*/
public function getEmailReplyTo()
{
return $this->emailReplyTo;
}
/**
* Set subject
*
* @param string $subject Email subject.
*
* @return EmailedDocument
*/
public function setSubject($subject)
{
$this->subject = $subject;
return $this;
}
/**
* Get subject
*
* @return string
*/
public function getSubject()
{
return $this->subject;
}
/**
* Set content
*
* @param string $content Email content.
*
* @return EmailedDocument
*/
public function setContent($content)
{
$this->content = $content;
return $this;
}
/**
* Get content
*
* @return string
*/
public function getContent()
{
return $this->content;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace AppBundle\Entity;
/**
* Interface EntityInterface
* @package AppBundle\Entity
*/
interface EntityInterface
{
/**
* Get id
*
* @return mixed
*/
public function getId();
/**
* Get entity type
*
* @return string
*/
public function getEntityType();
}
@@ -0,0 +1,63 @@
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use UserBundle\Entity\User;
/**
* Trait OwnerAwareEntityTrait
*
* Contains mapping for entities which should have owner relation with some user.
*
* @package AppBundle\Entity
*/
trait OwnerAwareEntityTrait
{
/**
* The user who created this notification.
*
* @var User
*
* @ORM\ManyToOne(targetEntity="UserBundle\Entity\User")
* @ORM\JoinColumn(name="owner_id", referencedColumnName="id", onDelete="SET NULL")
*/
protected $owner;
/**
* Set owner
*
* @param User $owner The owner of this notification.
*
* @return static
*/
public function setOwner(User $owner = null)
{
$this->owner = $owner;
return $this;
}
/**
* Get owner
*
* @return User
*/
public function getOwner()
{
return $this->owner;
}
/**
* Checks that this entity is owned by specified user.
*
* @param User $user A User entity instance.
*
* @return boolean
*/
public function isOwnedBy(User $user)
{
return $this->owner->getId() === $user->getId();
}
}
+171
View File
@@ -0,0 +1,171 @@
<?php
namespace AppBundle\Enum;
/**
* Class AbstractEnum
* @package AppBundle\Enum
*/
abstract class AbstractEnum
{
/**
* Cached constants.
*
* @var array
*/
private static $cache = [];
/**
* @var mixed
*/
protected $value;
/**
* AbstractEnum constructor.
*
* @param mixed $value One of availables enum values.
*/
public function __construct($value)
{
if (! self::isValid($value)) {
throw new \InvalidArgumentException(sprintf(
'Unknown value %s for enum %s. Expects one of %s',
$value,
static::class,
implode(', ', self::getAvailables())
));
}
$this->value = $value;
}
/**
* Get all available enum values.
*
* @return AbstractEnum[]
*/
public static function getValues()
{
$values = [];
foreach (static::getAvailables() as $available) {
//
// Code sniffer says that: 'Use parentheses when instantiating classes'
// but, obviously, we did it.
//
// @codingStandardsIgnoreStart
$values[] = new static($available);
// @codingStandardsIgnoreEnd
}
return $values;
}
/**
* Get available constants values.
*
* @return string[]
*/
public static function getAvailables()
{
$class = static::class;
if (! isset(self::$cache[$class])) {
$reflection = new \ReflectionClass($class);
self::$cache[$class] = $reflection->getConstants();
}
return self::$cache[$class];
}
/**
* Checks that specified value is valid for current enum.
*
* @param mixed $value Maybe one of enum value.
*
* @return boolean
*/
public static function isValid($value)
{
return in_array($value, self::getAvailables(), true);
}
/**
* @param string $name Method name.
* @param mixed $arguments Method arguments.
*
* @return static
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public static function __callStatic($name, $arguments)
{
// camelCase to underscore.
$name = strtoupper(preg_replace('/([^A-Z-])([A-Z])/', '$1_$2', $name));
$availables = self::getAvailables();
if (array_key_exists($name, $availables)) {
//
// Code sniffer says that: 'Use parentheses when instantiating classes'
// but, obviously, we did it.
//
// @codingStandardsIgnoreStart
return new static($availables[$name]);
// @codingStandardsIgnoreEnd
}
throw new \RuntimeException("Unknown enum value '{$name}'");
}
/**
* Checks that current value is equal to specified.
*
* @param AbstractEnum|string $enum One of availables enum values.
*
* @return boolean
*
* @SuppressWarnings(PHPMD.ShortMethodName)
*/
public function is($enum)
{
if (is_scalar($enum)) {
//
// Code sniffer says that: 'Use parentheses when instantiating classes'
// but, obviously, we did it.
//
// @codingStandardsIgnoreStart
$enum = new static($enum);
// @codingStandardsIgnoreEnd
}
if (! $enum instanceof static) {
throw new \InvalidArgumentException(sprintf(
'Unknown value %s for enum %s. Expects one of %s',
$enum,
static::class,
implode(', ', self::getAvailables())
));
}
return $this->value === $enum->getValue();
}
/**
* Get value.
*
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* @return string
*/
public function __toString()
{
return (string) $this->value;
}
}
@@ -0,0 +1,33 @@
<?php
namespace AppBundle\Enum;
/**
* Class UnhandledEnumException
*
* @package AppBundle\Enum
*/
class UnhandledEnumException extends \RuntimeException
{
/**
* UnhandledEnumException constructor.
*
* @param string $enumClass Enumeration class.
* @param mixed $value Unhandled value.
*/
public function __construct($enumClass, $value)
{
parent::__construct(sprintf('Unhandled \'%s\' enum value \'%s\'', $enumClass, $value));
}
/**
* @param AbstractEnum $enum A AbstractEnum instance.
*
* @return UnhandledEnumException
*/
public static function fromInstance(AbstractEnum $enum)
{
return new self(get_class($enum), $enum->getValue());
}
}
@@ -0,0 +1,59 @@
<?php
namespace AppBundle\EventListener;
use AppBundle\Response\SearchResponseInterface;
use Knp\Component\Pager\Event\ItemsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Class ResponsePagination
* Need for knp paginator.
*
* @package SearchBundle\EventListener
*/
class ResponsePagination implements EventSubscriberInterface
{
/**
* Returns an array of event names this subscriber wants to listen to.
*
* The array keys are event names and the value can be:
*
* * The method name to call (priority defaults to 0)
* * An array composed of the method name to call and the priority
* * An array of arrays composed of the method names to call and
* respective
* priorities, or 0 if unset
*
* For instance:
*
* * array('eventName' => 'methodName')
* * array('eventName' => array('methodName', $priority))
* * array('eventName' => array(array('methodName1', $priority),
* array('methodName2')))
*
* @return array The event names to listen to
*/
public static function getSubscribedEvents()
{
return [ 'knp_pager.items' => [ 'handle', 0 ] ];
}
/**
* @param ItemsEvent $event A ItemsEvent instance.
*
* @return void
*/
public function handle(ItemsEvent $event)
{
$response = $event->target;
if (! $response instanceof SearchResponseInterface) {
return;
}
$event->stopPropagation();
$event->items = $response->getDocuments();
$event->count = $response->getTotalCount();
}
}
@@ -0,0 +1,114 @@
<?php
namespace AppBundle\Exception;
use UserBundle\Entity\User;
use UserBundle\Enum\AppLimitEnum;
/**
* Class LimitExceedException
*
* Occurred when we user try to reserve more limits that allowed.
*
* @package AppBundle\Exception
*/
class LimitExceedException extends \RuntimeException
{
/**
* @var User
*/
private $user;
/**
* @var AppLimitEnum
*/
private $limit;
/**
* @var integer
*/
private $currValue;
/**
* @var integer
*/
private $requested;
/**
* @var integer
*/
private $max;
/**
* NotAllowedException constructor.
*
* @param User $user Who try to make not allowed action.
* @param AppLimitEnum $appLimit Which limit is requested.
* @param integer $currValue Current limit value.
* @param integer $requested How much limit is requested.
* @param integer $max Max value of limit.
*/
public function __construct(
User $user,
AppLimitEnum $appLimit,
$currValue,
$requested,
$max
) {
parent::__construct(sprintf(
'User \'%s\' is exceed limit \'%s\'. Current limit value %s, request %s but limit is %s',
$user->getId(),
$appLimit->getValue(),
$currValue,
$requested,
$max
));
$this->user = $user;
$this->limit = $appLimit;
$this->currValue = $currValue;
$this->requested = $requested;
$this->max = $max;
}
/**
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* @return AppLimitEnum
*/
public function getLimit()
{
return $this->limit;
}
/**
* @return integer
*/
public function getCurrValue()
{
return $this->currValue;
}
/**
* @return integer
*/
public function getRequested()
{
return $this->requested;
}
/**
* @return integer
*/
public function getMax()
{
return $this->max;
}
}
@@ -0,0 +1,61 @@
<?php
namespace AppBundle\Exception;
use UserBundle\Entity\User;
use UserBundle\Enum\AppPermissionEnum;
/**
* Class NotAllowedException
*
* Occurred when we user try to make operations which is not allowed for him.
*
* @package AppBundle\Exception
*/
class NotAllowedException extends \RuntimeException
{
/**
* @var User
*/
private $user;
/**
* @var AppPermissionEnum
*/
private $permission;
/**
* NotAllowedException constructor.
*
* @param User $user Who try to make not allowed action.
* @param AppPermissionEnum $appPermission Which permission is required.
*/
public function __construct(User $user, AppPermissionEnum $appPermission)
{
parent::__construct(sprintf(
'User \'%s\' is don\'t have \'%s\' permission',
$user->getId(),
$appPermission->getValue()
));
$this->user = $user;
$this->permission = $appPermission;
}
/**
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* @return AppPermissionEnum
*/
public function getPermission()
{
return $this->permission;
}
}
@@ -0,0 +1,64 @@
<?php
namespace AppBundle\Form;
use IndexBundle\Index\IndexInterface;
use IndexBundle\SearchRequest\SearchRequestBuilderInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class AbstractConnectionAwareType
* @package AppBundle\Form
*/
abstract class AbstractConnectionAwareType extends AbstractType
{
/**
* @var IndexInterface
*/
protected $index;
/**
* Max documents per page.
*
* @var integer
*/
protected $perPage;
/**
* SimpleQueryType constructor.
*
* @param IndexInterface $index A IndexInterface instance.
* @param integer $perPage Max documents per page.
*/
public function __construct(IndexInterface $index, $perPage)
{
$this->index = $index;
$this->perPage = $perPage;
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', SearchRequestBuilderInterface::class);
}
/**
* Create initial search request builder
*
* @return \IndexBundle\SearchRequest\SearchRequestBuilderInterface
*/
protected function createSearchRequestBuilder()
{
return $this->index
->createRequestBuilder()
->setLimit($this->perPage);
}
}
@@ -0,0 +1,57 @@
<?php
namespace AppBundle\Form;
use AppBundle\Entity\EmailedDocument;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class EmailedDocumentType
* @package AppBundle\Form
*/
class EmailedDocumentType extends AbstractType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('emailTo', CollectionType::class, [
'entry_type' => EmailType::class,
'allow_add' => true,
])
->add('emailReplyTo')
->add('subject', null, [ 'required' => false ])
->add('content');
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', EmailedDocument::class);
}
}
@@ -0,0 +1,61 @@
<?php
namespace AppBundle\Form\Factory;
use AppBundle\Form\AbstractConnectionAwareType;
use IndexBundle\Index\IndexInterface;
use IndexBundle\Index\Internal\InternalIndexInterface;
/**
* Class FilterFactoryAwareTypeFactory
* @package AppBundle\Form\Factory
*/
class FilterFactoryAwareTypeFactory
{
/**
* Max documents per page.
*
* @var integer
*/
private $perPage;
/**
* @var InternalIndexInterface
*/
private $index;
/**
* SimpleQueryType constructor.
*
* @param IndexInterface $index A IndexInterface interface.
* @param integer $perPage Max documents per page.
*/
public function __construct(
IndexInterface $index,
$perPage
) {
$this->index = $index;
$this->perPage = $perPage;
}
/**
* Create search type instance.
*
* @param string $class Concrete form fqcn.
*
* @return AbstractConnectionAwareType
*/
public function create($class)
{
$form = new $class($this->index, $this->perPage);
if (! $form instanceof AbstractConnectionAwareType) {
$message = 'Invalid form class, expects: '
. AbstractConnectionAwareType::class;
throw new \InvalidArgumentException($message);
}
return $form;
}
}
@@ -0,0 +1,90 @@
<?php
namespace AppBundle\Form;
use AppBundle\AdvancedFilters\AdvancedFiltersConfig;
use AppBundle\Form\Type\AdvancedFiltersType;
use Common\Enum\AFSourceEnum;
use IndexBundle\SearchRequest\SearchRequestBuilderInterface;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
/**
* Class FeedDocumentSearchType
* @package AppBundle\Form
*/
class FeedDocumentSearchType extends AbstractConnectionAwareType implements
DataMapperInterface
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// Page field, default value - 1
->add('page', null, [
'description' => 'Requested page number. Default value is 1.',
'empty_data' => 1,
])
// Collection of available advanced filters.
->add('advancedFilters', AdvancedFiltersType::class, [
'description' => 'Advanced filters.',
'config' => AdvancedFiltersConfig::getConfig(AFSourceEnum::FEED),
'empty_data' => [],
'connection' => $this->index,
'required' => false,
])
->setEmptyData($this->createSearchRequestBuilder())
->setDataMapper($this);
}
/**
* Maps properties of some data to a list of forms.
*
* @param mixed $data Structured data.
* @param FormInterface[]|\RecursiveIteratorIterator $forms A list of
* {@link FormInterface}
* instances.
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function mapDataToForms($data, $forms)
{
// Do nothing because it's senseless.
}
/**
* Maps the data of a list of forms into the properties of some data.
*
* @param FormInterface[]|\RecursiveIteratorIterator $forms A list of
* {@link FormInterface}
* instances.
* @param mixed|SearchRequestBuilderInterface $data Structured data.
*
* @return void
*/
public function mapFormsToData($forms, &$data)
{
$forms = iterator_to_array($forms);
$data
->setPage($forms['page']->getData())
->setFilters($forms['advancedFilters']->getData());
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace AppBundle\Form;
use AppBundle\Form\SearchRequest\StoredQuerySearchRequestType;
use CacheBundle\Entity\Feed\ClipFeed;
use CacheBundle\Form\FeedInfoType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Validator\Constraints\Valid;
/**
* Class FeedType
* @package AppBundle\Form
*/
class FeedType extends AbstractType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('feed', FeedInfoType::class, [
'constraints' => new Valid(),
])
->add('search', StoredQuerySearchRequestType::class)
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
$form = $event->getForm()->get('search');
if (! isset($data['feed']['subType'])) {
return;
}
if ($data['feed']['subType'] === ClipFeed::getSubType()) {
$form
->remove('query')
->remove('filters');
}
});
}
}
@@ -0,0 +1,177 @@
<?php
namespace AppBundle\Form\SearchRequest;
use AppBundle\AdvancedFilters\AdvancedFiltersConfig;
use AppBundle\Form\AbstractConnectionAwareType;
use AppBundle\Form\Type\AdvancedFiltersType;
use AppBundle\Form\Type\Filter as QueryFilter;
use AppBundle\Form\Type\FiltersType;
use Common\Enum\AFSourceEnum;
use IndexBundle\SearchRequest\SearchRequestBuilderInterface;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* Class AbstractSearchRequestType
* @package AppBundle\Form\SearchRequest
*/
abstract class AbstractSearchRequestType extends AbstractConnectionAwareType implements
DataMapperInterface
{
/**
* Available search request filters for articles.
*
* @var array
*/
public static $filters = [
'headline' => [
'type' => QueryFilter\HeadlineFilterType::class,
'description' => 'Addition title filtering',
],
'publisher' => [
'type' => QueryFilter\PublisherFilterType::class,
'description' => 'Filter by publisher type.',
],
'source' => [
'type' => QueryFilter\SourceFilterType::class,
'description' => 'Filter by source.',
],
'sourceList' => [
'type' => QueryFilter\SourceListFilterType::class,
'description' => 'Filter by source lists.',
],
'language' => [
'type' => QueryFilter\LanguageFilterType::class,
'description' => 'Filter by language, use ISO 639-1 two-letters codes.',
],
'country' => [
'type' => QueryFilter\CountryFilterType::class,
'description' => 'Filter by countries, ISO 3166-1 Alpha-2 two-letters codes.',
],
'state' => [
'type' => QueryFilter\StateFilterType::class,
'description' => 'Filter by US states, ANSI standard INCITS 38:2009 two-letters codes.',
],
'date' => [
'type' => QueryFilter\DateFilterType::class,
'description' => 'Filter by date, may be two types',
],
'hasImage' => [
'type' => QueryFilter\HasImageFilterType::class,
'description' => 'Boolean flag, if true get document only with image.',
],
];
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// Raw search query typed by user.
->add('query', null, [
'constraints' => [
new NotBlank(),
new Length([ 'min' => 3 ]),
],
'required' => true,
'description' => 'Search query.',
])
// Collection of available filters.
->add('filters', FiltersType::class, [
'filter_factory' => $this->index->getFilterFactory(),
'description' => 'Search filters.',
'empty_data' => [],
'filters' => self::$filters,
'required' => false,
])
// Collection of available advanced filters.
->add('advancedFilters', AdvancedFiltersType::class, [
'description' => 'Advanced filters.',
'config' => AdvancedFiltersConfig::getConfig(AFSourceEnum::FEED),
'empty_data' => [],
'connection' => $this->index,
'required' => false,
])
->setEmptyData($this->createSearchRequestBuilder())
->setDataMapper($this);
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'data_class' => SearchRequestBuilderInterface::class,
'key' => 'createFeed',
]);
}
/**
* Maps properties of some data to a list of forms.
*
* @param mixed $data Structured data.
* @param FormInterface[]|\RecursiveIteratorIterator $forms A list of
* {@link FormInterface}
* instances.
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function mapDataToForms($data, $forms)
{
// Do nothing because it's senseless.
}
/**
* Maps the data of a list of forms into the properties of some data.
*
* @param FormInterface[]|\RecursiveIteratorIterator $forms A list of
* {@link FormInterface}
* instances.
* @param mixed|SearchRequestBuilderInterface $data Structured data.
*
* @return void
*/
public function mapFormsToData($forms, &$data)
{
$forms = iterator_to_array($forms);
if (isset($forms['query'])) {
$data->setQuery($forms['query']->getData());
}
$filters = $forms['advancedFilters']->getData() ?: [];
if (isset($forms['filters'])) {
$filters = array_merge($filters, $forms['filters']->getData());
}
$data->setFilters($filters);
}
}
@@ -0,0 +1,72 @@
<?php
namespace AppBundle\Form\SearchRequest;
use IndexBundle\SearchRequest\SearchRequestBuilderInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Test\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class ClipFeedSearchRequestType
* @package AppBundle\Form\SearchRequest
*/
class ClipFeedSearchRequestType extends AbstractSearchRequestType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->remove('query')
->remove('filters');
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'cascade_validation' => true,
'key' => 'search',
]);
}
/**
* Maps the data of a list of forms into the properties of some data.
*
* @param FormInterface[]|\RecursiveIteratorIterator $forms A list of
* {@link FormInterface}
* instances.
* @param mixed|SearchRequestBuilderInterface $data Structured data.
*
* @return void
*/
public function mapFormsToData($forms, &$data)
{
$forms = iterator_to_array($forms);
$data->setFilters($forms['advancedFilters']->getData());
}
}
@@ -0,0 +1,76 @@
<?php
namespace AppBundle\Form\SearchRequest;
use AppBundle\Form\Type\Filter as QueryFilter;
use IndexBundle\SearchRequest\SearchRequestBuilderInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class SimpleQuerySearchRequestType
* @package AppBundle\Form\SearchRequest
*/
class SimpleQuerySearchRequestType extends AbstractSearchRequestType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// Page field, default value - 1
->add('page', null, [
'description' => 'Requested page number. Default value is 1.',
'empty_data' => 1,
]);
parent::buildForm($builder, $options);
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'cascade_validation' => true,
'key' => 'search',
]);
}
/**
* Maps the data of a list of forms into the properties of some data.
*
* @param FormInterface[]|\RecursiveIteratorIterator $forms A list of
* {@link FormInterface}
* instances.
* @param mixed|SearchRequestBuilderInterface $data Structured data.
*
* @return void
*/
public function mapFormsToData($forms, &$data)
{
parent::mapFormsToData($forms, $data);
$forms = iterator_to_array($forms);
$data->setPage($forms['page']->getData());
}
}
@@ -0,0 +1,27 @@
<?php
namespace AppBundle\Form\SearchRequest;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class StoredQuerySearchRequestType
* @package AppBundle\Form\SearchRequest
*/
class StoredQuerySearchRequestType extends AbstractSearchRequestType
{
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefault('key', 'createFeed');
}
}
@@ -0,0 +1,25 @@
<?php
namespace AppBundle\Form\Transformer;
use Symfony\Component\Form\CallbackTransformer;
/**
* Class OnlyReverseTransformer
* @package AppBundle\Form\Transformer
*/
class OnlyReverseTransformer extends CallbackTransformer
{
/**
* OnlyReverseTransformer constructor.
*
* @param callable $reverseTransform The reverse transform callback.
*/
public function __construct(callable $reverseTransform)
{
parent::__construct(function ($value) {
return $value;
}, $reverseTransform);
}
}
@@ -0,0 +1,51 @@
<?php
namespace AppBundle\Form\Transformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Trait OnlyReverseTransformer
* @package AppBundle\Form\Transformer
*/
trait OnlyReverseTransformerTrait
{
/**
* Transforms a value from the original representation to a transformed
* representation.
*
* This method is called on two occasions inside a form field:
*
* 1. When the form field is initialized with the data attached from the
* datasource (object or array).
* 2. When data from a request is submitted using {@link Form::submit()} to
* transform the new input data back into the renderable format. For
* example if you have a date field and submit '2009-10-10' you might
* accept this value because its easily parsed, but the transformer still
* writes back
* "2009/10/10" onto the form field (for further displaying or other
* purposes).
*
* This method must be able to deal with empty values. Usually this will
* be NULL, but depending on your implementation other empty values are
* possible as well (such as empty strings). The reasoning behind this is
* that value transformers must be chainable. If the transform() method
* of the first value transformer outputs NULL, the second value
* transformer
* must be able to process that value.
*
* By convention, transform() should return an empty string if NULL is
* passed.
*
* @param mixed $value The value in the original representation.
*
* @return mixed The value in the transformed representation
*
* @throws TransformationFailedException When the transformation fails.
*/
public function transform($value)
{
return $value;
}
}
@@ -0,0 +1,165 @@
<?php
namespace AppBundle\Form\Type\AdvancedFilter;
use IndexBundle\Filter\Factory\FilterFactoryInterface;
use IndexBundle\Filter\FilterInterface;
/**
* Class AdvancedFilterParameters
* @package AppBundle\Form\Type\AdvancedFilter
*/
class AdvancedFilterParameters
{
/**
* Array of values which MUST be present in the search result.
*
* @var array
*/
private $included;
/**
* Array of values which MUST NOT be present in the search result.
*
* @var string[]
*/
private $excluded;
/**
* AdvancedFilterParameters constructor.
*
* @param string|string[] $included Array of values which MUST be present in
* the search result.
* @param string|string[] $excluded Array of values which MUST NOT be present
* in the search result.
*/
public function __construct($included, $excluded)
{
$this->included = (array) $included;
$this->excluded = (array) $excluded;
}
/**
* @param string $value Additional query.
*
* @return AdvancedFilterParameters
*/
public static function queryFilterParameters($value)
{
// @codingStandardsIgnoreStart
return new static([ $value ], []);
// @codingStandardsIgnoreEnd
}
/**
* Get array of values which MUST be present in the search result.
*
* @return string[]
*/
public function getIncluded()
{
return $this->included;
}
/**
* Get array of values which MUST NOT be present in the search result.
*
* @return string[]
*/
public function getExcluded()
{
return $this->excluded;
}
/**
* @param array $names Array of used field names.
* @param FilterFactoryInterface $factory A FilterFactoryInterface instance.
*
* @return FilterInterface
*/
public function createQueryFilter(array $names, FilterFactoryInterface $factory)
{
return $factory->orX(array_map(function ($name) use ($factory) {
return $factory->eq($name, current($this->included));
}, $names));
}
/**
* Create filter for range advanced filter.
*
* @param string $name A Field name.
* @param array $ranges Available field ranges.
* @param FilterFactoryInterface $factory A FilterFactoryInterface instance.
*
* @return FilterInterface
*/
public function createRangeFilter($name, array $ranges, FilterFactoryInterface $factory)
{
$availables = array_keys($ranges);
(($value = current($this->included)) !== false) || ($value = current($this->excluded));
if (! in_array($value, $availables, true)) {
throw new \InvalidArgumentException(sprintf(
'Invalid value \'%s\'. Expects one of %s.',
$value,
implode(', ', $availables)
));
}
//
// Get start and end bound for given value.
//
$start = $ranges[$value]['from'];
$end = isset($ranges[$value]['to']) ? $ranges[$value]['to'] : null;
// Firstly create 'gte' filter.
$filter = $factory->gte($name, $start);
if ($end !== null) {
//
// We have end bound, so we should use 'lte' filter and wrap
// both into 'andX'.
//
$filter = $factory->andX([
$filter,
$factory->lte($name, $end),
]);
}
return $filter;
}
/**
* Create filter for simple advanced filter.
*
* @param string $name A Field name.
* @param FilterFactoryInterface $factory A FilterFactoryInterface instance.
*
* @return FilterInterface|null
*/
public function createSimpleFilter($name, FilterFactoryInterface $factory)
{
$eqFactory = \nspl\f\partial([ $factory, 'eq' ], $name);
$eqFilters = \nspl\a\map($eqFactory, $this->included);
//
// If we got some positive statements we should use only them.
//
if (count($eqFilters) > 0) {
return $factory->orX($eqFilters);
}
$neqFactory = \nspl\f\compose(
[ $factory, 'not' ],
$eqFactory
);
$neqFilters = \nspl\a\map($neqFactory, $this->excluded);
if (count($neqFilters) > 0) {
return $factory->andX($neqFilters);
}
return null;
}
}
@@ -0,0 +1,107 @@
<?php
namespace AppBundle\Form\Type\AdvancedFilter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class AdvancedFilterType
*
* Convert filter parameters passed from frontend into internal representation.
*
* @package AppBundle\Form\Type\AdvancedFilte
*
* @see \AppBundle\Form\Type\AdvancedFilter\AdvancedFilterParameters
*/
class AdvancedFilterType extends AbstractType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = $options['choices'];
$transformer = function (FormEvent $event) use ($choices) {
$values = $event->getData();
if ($values instanceof AdvancedFilterParameters) {
$values = $event->getForm()->getExtraData();
}
$include = [];
$exclude = [];
switch (true) {
case is_string($values):
$data = AdvancedFilterParameters::queryFilterParameters($values);
break;
case is_array($values):
if (count($choices) > 0) {
// Range type.
$include = key($values);
} else {
// Simple type.
foreach ($values as $value => $type) {
switch ($type) {
case 1:
$include[] = $value;
break;
case -1:
$exclude[] = $value;
break;
default:
throw new \RuntimeException('Invalid value type, should be 1 or -1');
}
}
}
$data = new AdvancedFilterParameters($include, $exclude);
break;
default:
$event->getForm()->addError(new FormError('Invalid value, expects object or string'));
return;
}
$event->setData($data);
};
$builder->addEventListener(FormEvents::SUBMIT, $transformer);
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => AdvancedFilterParameters::class,
'empty_data' => new AdvancedFilterParameters([], []),
'allow_extra_fields' => true,
'choices' => [],
'compound' => true,
]);
}
}
@@ -0,0 +1,118 @@
<?php
namespace AppBundle\Form\Type;
use AppBundle\Form\Transformer\OnlyReverseTransformer;
use AppBundle\Form\Type\AdvancedFilter\AdvancedFilterParameters;
use AppBundle\Form\Type\AdvancedFilter\AdvancedFilterType;
use AppBundle\Form\Type\Traits\CleanFormTrait;
use Common\Enum\AFTypeEnum;
use IndexBundle\Index\IndexInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* Class AdvancedFiltersType
* @package AppBundle\Form\Type
*/
class AdvancedFiltersType extends AbstractType
{
use CleanFormTrait;
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$index = $options['connection'];
if (! $index instanceof IndexInterface) {
throw new \RuntimeException(sprintf(
'\'connection\' options should be instance of %s',
IndexInterface::class
));
}
$config = array_filter($options['config']);
//
// Add all available filters to form.
//
foreach ($config as $name => $params) {
$parameters = [];
if ($params['type'] === AFTypeEnum::RANGE) {
$parameters['choices'] = array_keys($params['ranges']);
} elseif ($params['type'] === AFTypeEnum::QUERY) {
$parameters['compound'] = false;
}
$parameters['description'] = $params['description'];
$parameters['constraints'] = new NotBlank();
$builder->add($name, AdvancedFilterType::class, $parameters);
}
/**
* Transform filter names and values to concrete filters.
*
* @param array $filters Array of advanced filters values.
*
* @return \IndexBundle\Filter\FilterInterface[]
*/
$transformationFn = function (array $filters) use ($index, $config) {
$resolver = $index->getAFResolver();
$resolvedFilters = [];
/** @var AdvancedFilterParameters $params */
foreach ($filters as $name => $params) {
try {
$resolvedFilters[] = $resolver->generateFilter($config, $name, $params);
} catch (\Exception $exception) {
throw new TransformationFailedException($exception->getMessage(), $exception->getCode(), $exception);
}
}
return $resolvedFilters;
};
$builder
->addEventListener(FormEvents::PRE_SUBMIT, [ $this, 'clean' ])
->addModelTransformer(new OnlyReverseTransformer($transformationFn));
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setRequired('connection')
->setRequired('config')
->setAllowedTypes('connection', IndexInterface::class)
->setAllowedTypes('config', 'array')
->setDefaults([
'allow_extra_fields' => true, // We handle this situation in pre
// submit listener and make more
// detailed error message.
'connection' => null,
]);
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace AppBundle\Form\Type;
use AppBundle\Enum\AbstractEnum;
use AppBundle\Form\Transformer\OnlyReverseTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class EnumType
*
* @package AppBundle\Form\Type
*/
class EnumType extends AbstractType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$class = $options['enum_class'];
$builder->addModelTransformer(new OnlyReverseTransformer(function ($value) use ($class) {
if ($value === null) {
return null;
}
return new $class($value);
}));
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setRequired('enum_class')
->setAllowedTypes('enum_class', 'string')
->setDefault('choices', function (Options $options) {
$class = $options['enum_class'];
if (! class_exists($class)) {
throw new \InvalidArgumentException('Can\'t find class: '. $class);
}
$reflection = new \ReflectionClass($class);
if ($reflection->isSubclassOf(AbstractEnum::class)) {
return $class::getAvailables();
}
return [];
});
}
/**
* Returns the name of the parent type.
*
* @return string|null The name of the parent type if any, null otherwise.
*/
public function getParent()
{
return ChoiceType::class;
}
}
@@ -0,0 +1,56 @@
<?php
namespace AppBundle\Form\Type\Extension;
use AppBundle\Utils\TransKey\ConstTransKeyGenerator;
use AppBundle\Utils\TransKey\RecursiveTransKeyGenerator;
use AppBundle\Utils\TransKey\TransKeyGeneratorInterface;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class LocalizationTypeExtension
* @package AppBundle\Form\Type\Extension
*/
class LocalizationTypeExtension extends AbstractTypeExtension
{
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('key', new RecursiveTransKeyGenerator())
->addAllowedTypes('key', [ 'string', TransKeyGeneratorInterface::class ])
->setNormalizer('key', function (Options $options, $key) {
//
// We should insure that key is always be an instance of trans key
// generator.
//
if (is_string($key)) {
$key = new ConstTransKeyGenerator($key);
}
return $key;
});
}
/**
* Returns the name of the type being extended.
*
* @return string The name of the type being extended.
*/
public function getExtendedType()
{
return FormType::class;
}
}
@@ -0,0 +1,90 @@
<?php
namespace AppBundle\Form\Type\Filter;
use AppBundle\Form\Transformer\OnlyReverseTransformer;
use IndexBundle\Filter\Factory\FilterFactoryInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class AbstractFilterType
* @package AppBundle\Form\Type\Filter
*/
abstract class AbstractFilterType extends AbstractType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** @var FilterFactoryInterface $factory */
$factory = $options['filter_factory'];
if (! $factory instanceof FilterFactoryInterface) {
throw new \RuntimeException(sprintf(
'\'filter_factory\' option should be instance of %s',
FilterFactoryInterface::class
));
}
$builder
->addModelTransformer(new OnlyReverseTransformer(function ($value) use ($factory) {
return $this->transform($value, $factory);
}))
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$this->preSubmit($event);
});
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setRequired('filter_factory')
->setAllowedTypes('filter_factory', FilterFactoryInterface::class);
}
/**
* Make some manipulation with form and data before submit.
*
* @param FormEvent $event A FormEvent instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function preSubmit(FormEvent $event)
{
// do nothing.
}
/**
* Transform input values into proper filters.
*
* @param mixed $value Value to be transformed.
* @param FilterFactoryInterface $factory A FilterFactoryInterface instance.
*
* @return \IndexBundle\Filter\FilterInterface|null
*/
abstract protected function transform($value, FilterFactoryInterface $factory);
}
@@ -0,0 +1,80 @@
<?php
namespace AppBundle\Form\Type\Filter;
use Common\Enum\CountryEnum;
use Common\Enum\FieldNameEnum;
use IndexBundle\Filter\Factory\FilterFactoryInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class CountryFilterType
* @package AppBundle\Form\Type\Filter
*/
class CountryFilterType extends AbstractFilterType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('include', ChoiceType::class, [
'choices' => CountryEnum::getAvailables(),
'multiple' => true,
'description' => 'Get document within specified countries',
])
->add('exclude', ChoiceType::class, [
'choices' => CountryEnum::getAvailables(),
'multiple' => true,
'description' => 'Get document not within specified countries',
]);
}
/**
* Transform input values into proper filters.
*
* @param mixed $value Value to be transformed.
* @param FilterFactoryInterface $factory A FilterFactoryInterface instance.
*
* @return \IndexBundle\Filter\FilterInterface|null
*/
protected function transform($value, FilterFactoryInterface $factory)
{
if (! isset($value['include'], $value['exclude'])) {
//
// One of value is not valid, don't make any transformations.
//
return null;
}
$include = $value['include'];
$exclude = $value['exclude'];
$condition = $factory->andX();
if (count($include) > 0) {
$condition->add($factory->in(FieldNameEnum::COUNTRY, $include));
}
if (count($exclude) > 0) {
$condition->add($factory->not($factory->in(FieldNameEnum::COUNTRY, $exclude)));
}
return $condition;
}
}
@@ -0,0 +1,215 @@
<?php
namespace AppBundle\Form\Type\Filter;
use Common\Enum\FieldNameEnum;
use IndexBundle\Filter\Factory\FilterFactoryInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Validator\Constraints\GreaterThan;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* Class DateFilterType
* @package AppBundle\Form\Type\Filter
*/
class DateFilterType extends AbstractFilterType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('type', ChoiceType::class, [
'choices' => [ 'last', 'between' ],
'description' => 'Date filter type.',
])
->add('days', IntegerType::class, [
'constraints' => [
new GreaterThan([
'value' => 0,
'message' => 'This value should be integer greater than 0.',
]),
new NotBlank(),
],
'invalid_message' => 'This value should be integer greater than 0.',
'description' => 'How many days ago document found. Used only for \'last\' type.',
])
->add('start', DateType::class, [
'widget' => 'single_text',
'input' => 'string',
'description' => 'Start of searched period, format: \'YYYY-MM-DD\'. Used only for \'between\' type.',
'constraints' => new NotBlank(),
])
->add('end', DateType::class, [
'widget' => 'single_text',
'input' => 'string',
'description' => 'End of searched period, format: \'YYYY-MM-DD\'. Used only for \'between\' type.',
'constraints' => new NotBlank(),
]);
}
/**
* Change form based on selected type.
*
* @param FormEvent $event A FormEvent instance.
*
* @return void
*/
protected function preSubmit(FormEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
switch (true) {
//
// Stop processing because type not set.
// Remove all fields to avoid unnecessary validation errors.
//
case ! isset($data['type']) || (($data['type'] !== 'last')
&& ($data['type'] !== 'between')):
$form
->remove('days')
->remove('start')
->remove('end');
break;
//
// For last 'type' we should remove 'start' and 'end' field because
// its unnecessary.
//
case $data['type'] === 'last':
$form
->remove('start')
->remove('end');
break;
//
// Make additional validation for 'between' type.
//
// Try to convert 'start' and 'end' values into datetime instances
// and check that 'start' not greater than 'end' and if it so add
// proper error message to 'start' field.
//
default:
if (isset($data['start'], $data['end'])) {
$start = date_create_from_format('Y-m-d', $data['start'])->setTime(0, 0);
$end = date_create_from_format('Y-m-d', $data['end'])->setTime(0, 0);
if ((($start !== false) && ($end !== false)) && ($start > $end)) {
$form->addError(new FormError(
'\'start\' value should be less than \'end\'.'
));
}
}
$form->remove('days');
}
}
/**
* Transform input values into proper filters.
*
* @param mixed $value Value to be transformed.
* @param FilterFactoryInterface $factory A FilterFactoryInterface instance.
*
* @return \IndexBundle\Filter\FilterInterface|null
*/
protected function transform($value, FilterFactoryInterface $factory)
{
//
// Don't make any transformations if 'type' is not set.
//
if (! is_array($value) || ! isset($value['type'])) {
return null;
}
//
// Because transformation occurred before validation we should check all
// values and if some of them is invalid we just return null.
//
// All validation error messages will be added in form validation
// listener so we should'nt worry about it.
//
switch ($value['type']) {
case 'last':
return $this->transformLast($value, $factory);
case 'between':
return $this->transformBetween($value, $factory);
}
throw new \LogicException(sprintf(
'Unhandled date filter type \'%s\'',
$value['type']
));
}
/**
* @param array $value Date filter value.
* @param FilterFactoryInterface $factory A FilterFactoryInterface instance.
*
* @return \IndexBundle\Filter\Filters\AndFilter|null
*/
private function transformLast(array $value, FilterFactoryInterface $factory)
{
if (! isset($value['days']) || ($value['days'] < 0)) {
return null;
}
return $factory->andX([
$factory->gte(FieldNameEnum::PUBLISHED, date_create(sprintf(
'- %d days 00:00:00',
$value['days']
))),
$factory->lte(FieldNameEnum::PUBLISHED, date_create('23:59:59')),
]);
}
/**
* @param array $value Date filter value.
* @param FilterFactoryInterface $factory A FilterFactoryInterface instance.
*
* @return \IndexBundle\Filter\Filters\AndFilter|null
*/
private function transformBetween(array $value, FilterFactoryInterface $factory)
{
if (! isset($value['start'], $value['end'])) {
return null;
}
// Try to create datetime instances from 'start' and 'end' fields and
// if we got error we should stop further transformations and return
// null.
$start = date_create_from_format('Y-m-d', $value['start'])->setTime(0, 0);
$end = date_create_from_format('Y-m-d', $value['end'])->setTime(23, 59, 59);
if (($start === false) || ($end === false)) {
return null;
}
return $factory->andX([
$factory->gte(FieldNameEnum::PUBLISHED, $start),
$factory->lte(FieldNameEnum::PUBLISHED, $end),
]);
}
}
@@ -0,0 +1,64 @@
<?php
namespace AppBundle\Form\Type\Filter;
use Common\Enum\FieldNameEnum;
use IndexBundle\Filter\Factory\FilterFactoryInterface;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
/**
* Class HasImageFilterType
*
* If false we should search documents with images and without.
* If true we should search documents only with images.
*
* @package AppBundle\Form\Type\Filter
*/
class HasImageFilterType extends AbstractFilterType
{
/**
* Returns the name of the parent type.
*
* @return string|null The name of the parent type if any, null otherwise.
*/
public function getParent()
{
return CheckboxType::class;
}
/**
* Make some manipulation with form and data before submit.
*
* @param FormEvent $event A FormEvent instance.
*
* @return void
*/
protected function preSubmit(FormEvent $event)
{
$data = trim($event->getData());
if (($data !== '0') && ($data !== '') && ($data !== '1')) {
$event->getForm()->addError(
new FormError('This value should be of type boolean.')
);
}
}
/**
* Transform input values into proper filters.
*
* @param mixed $value Value to be transformed.
* @param FilterFactoryInterface $factory A FilterFactoryInterface instance.
*
* @return \IndexBundle\Filter\FilterInterface|null
*/
protected function transform($value, FilterFactoryInterface $factory)
{
return $value === true
? $factory->eq(FieldNameEnum::IMAGE_SRC, '/.+/')
: null;
}
}
@@ -0,0 +1,74 @@
<?php
namespace AppBundle\Form\Type\Filter;
use Common\Enum\FieldNameEnum;
use IndexBundle\Filter\Factory\FilterFactoryInterface;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class HeadlineFilterType
* @package AppBundle\Form\Type\Filter
*/
class HeadlineFilterType extends AbstractFilterType
{
/**
* Builds the form.
*
* This method is called for each type in the hierarchy starting from the
* top most type. Type extensions can further modify the form.
*
* @see FormTypeExtensionInterface::buildForm()
*
* @param FormBuilderInterface $builder The form builder.
* @param array $options The options.
*
* @return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('include', null, [
'description' => 'Comma separated list of words which should be in title.',
])
->add('exclude', null, [
'description' => 'Comma separated list of words which should not be in title.',
]);
}
/**
* Transform input values into proper filters.
*
* @param mixed $value Value to be transformed.
* @param FilterFactoryInterface $factory A FilterFactoryInterface instance.
*
* @return \IndexBundle\Filter\FilterInterface|null
*/
protected function transform($value, FilterFactoryInterface $factory)
{
if (! isset($value['include']) && !isset($value['exclude'])) {
//
// One of value is not valid, don't make any transformations.
//
return null;
}
$include = array_filter(array_map('trim', explode(',', $value['include'])));
$exclude = array_filter(array_map('trim', explode(',', $value['exclude'])));
$condition = $factory->andX();
if (count($include) > 0) {
$condition->add($factory->in(FieldNameEnum::TITLE, $include));
}
if (count($exclude) > 0) {
$condition->add($factory->not($factory->in(FieldNameEnum::TITLE, $exclude)));
}
return $condition;
}
}
@@ -0,0 +1,61 @@
<?php
namespace AppBundle\Form\Type\Filter;
use Common\Enum\FieldNameEnum;
use Common\Enum\LanguageEnum;
use IndexBundle\Filter\Factory\FilterFactoryInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class LanguageFilterType
* @package AppBundle\Form\Type\Filter
*/
class LanguageFilterType extends AbstractFilterType
{
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'choices' => LanguageEnum::getAvailables(),
'multiple' => true,
]);
}
/**
* Returns the name of the parent type.
*
* @return string|null The name of the parent type if any, null otherwise.
*/
public function getParent()
{
return ChoiceType::class;
}
/**
* Transform input values into proper filters.
*
* @param mixed $value Value to be transformed.
* @param FilterFactoryInterface $factory A FilterFactoryInterface instance.
*
* @return \IndexBundle\Filter\FilterInterface|null
*/
protected function transform($value, FilterFactoryInterface $factory)
{
if (! is_array($value) || (count($value) === 0)) {
return null;
}
return $factory->in(FieldNameEnum::LANG, $value);
}
}

Some files were not shown because too many files have changed in this diff Show More