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
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace CacheBundle;
use CacheBundle\DependencyInjection\Compiler\CollectFeedFetchersCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* Class CacheBundle
* @package CacheBundle
*/
class CacheBundle extends Bundle
{
/**
* Builds the bundle.
*
* It is only ever called once when the cache is empty.
*
* @param ContainerBuilder $container A ContainerBuilder instance.
*
* @return void
*/
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new CollectFeedFetchersCompilerPass());
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace CacheBundle;
/**
* Class CacheBundleServices
* @package CacheBundle
*/
abstract class CacheBundleServices
{
/**
* Cache storage.
*
* Implements {@see \CacheBundle\Cache\CacheInterface} interface.
*/
const CACHE = 'app.feed_manager';
/**
* Source cache storage.
*
* Implements {@see \CacheBundle\Cache\SourceCacheInterface} interface.
*/
const SOURCE_CACHE = 'app.source_manager';
/**
* Feed fetcher factory.
*
* Implements {@see \CacheBundle\Feed\Fetcher\Factory\FeedFetcherFactoryInterface}
* interface.
*/
const FEED_FETCHER_FACTORY = 'cache.feed_fetcher_factory';
/**
* Comment manager.
*
* Implements {@see \CacheBundle\Document\Extractor\DocumentContentExtractorInterface}
* interface.
*/
const DOCUMENT_CONTENT_EXTRACTOR = 'cache.document_content_extractor';
}
@@ -0,0 +1,93 @@
<?php
namespace CacheBundle\Command;
use CacheBundle\Entity\Page;
use CacheBundle\Entity\Query\SimpleQuery;
use CacheBundle\Repository\SimpleQueryRepository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class RemoveOldQueriesCommand
* @package CacheBundle\Command
*/
class RemoveOldQueriesCommand extends Command
{
const NAME = 'socialhose:query:remove_old';
/**
* @var EntityManagerInterface
*/
private $em;
/**
* RemoveOldQueriesCommand constructor.
*
* @param EntityManagerInterface $em A EntityManagerInterface instance.
*/
public function __construct(EntityManagerInterface $em)
{
parent::__construct(self::NAME);
$this->em = $em;
}
/**
* Configures the current command.
*
* @return void
*/
protected function configure()
{
$this->setDescription('Remove old queries from cache');
}
/**
* 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)
{
/** @var SimpleQueryRepository $queryRepository */
$queryRepository = $this->em->getRepository(SimpleQuery::class);
/** @var EntityRepository $pageRepository */
$pageRepository = $this->em->getRepository(Page::class);
$expr = $this->em->getExpressionBuilder();
$ids = $queryRepository->getOld();
$pageRepository->createQueryBuilder('Page')
->delete()
->where($expr->in('Page.query', $ids))
->getQuery()
->execute();
$queryRepository->createQueryBuilder('Query')
->delete()
->where($expr->in('Query.id', $ids))
->getQuery()
->execute();
return 0;
}
}
@@ -0,0 +1,55 @@
<?php
namespace CacheBundle\Comment\Manager;
use CacheBundle\Entity\Comment;
use CacheBundle\Entity\Document;
use CacheBundle\Repository\CommentRepository;
use Doctrine\ORM\EntityManagerInterface;
/**
* Class CommentManager
* @package CacheBundle\Comment\Manager
*/
class CommentManager implements CommentManagerInterface
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* CommentManager constructor.
*
* @param EntityManagerInterface $em A EntityManagerInterface instance.
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* Add new comment to specified entity.
*
* @param Comment $comment A Comment entity instance.
* @param Document $document A Document entity instance.
*
* @return Comment
*/
public function addComment(Comment $comment, Document $document)
{
$comment->setDocument($document);
$document->incCommentsCount();
$this->em->persist($document);
$this->em->persist($comment);
$this->em->flush();
/** @var CommentRepository $repository */
$repository = $this->em->getRepository(Comment::class);
$repository->updateCommentMarks($document->getId(), self::NEW_COMMENT_POOL_SIZE);
return $comment;
}
}
@@ -0,0 +1,29 @@
<?php
namespace CacheBundle\Comment\Manager;
use CacheBundle\Entity\Comment;
use CacheBundle\Entity\Document;
/**
* Interface CommentManagerInterface
* @package CacheBundle\Comment\Manager
*/
interface CommentManagerInterface
{
/**
* Size of new comments pool.
*/
const NEW_COMMENT_POOL_SIZE = 1;
/**
* Add new comment to specified entity.
*
* @param Comment $comment A Comment entity instance.
* @param Document $document A Document entity instance.
*
* @return Comment
*/
public function addComment(Comment $comment, Document $document);
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace CacheBundle\DTO;
use CacheBundle\Entity\Feed\AbstractFeed;
use IndexBundle\Filter\FilterInterface;
use UserBundle\Entity\User;
/**
* Class AnalyticDTO
*
* Contains all data which is used for creating Analytic and AnalyticContext entities.
*
* @package CacheBundle\DTO
*/
class AnalyticDTO
{
/**
* Used feeds as source of data for analyzes.
*
* @var AbstractFeed[]
*/
public $feeds;
/**
* Analytic owner.
*
* @var User
*/
public $owner;
/**
* Additional filters for data from feeds.
*
* @var FilterInterface[]
*/
public $filters;
/**
* Additional filters as is it's passed from frontend.
*
* @var array
*/
public $rawFilters;
/**
* AnalyticDTO constructor.
*
* @param array $feeds Used feeds as source of data for
* analyzes.
* @param User $owner Analytic owner.
* @param FilterInterface[] $filters Additional filters for data from feeds.
* @param array $rawFilters Additional filters as is it's passed
* from frontend.
*/
public function __construct(
array $feeds = [],
User $owner = null,
array $filters = [],
array $rawFilters = []
) {
$this->feeds = $feeds;
$this->owner = $owner;
$this->filters = $filters;
$this->rawFilters = $rawFilters;
}
}
@@ -0,0 +1,65 @@
<?php
namespace CacheBundle\DependencyInjection\Compiler;
use CacheBundle\Feed\Fetcher\Factory\LazyFeedFetcherFactory;
use CacheBundle\Feed\Fetcher\FeedFetcherInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Class CollectFeedFetchersCompilerPass
* @package CacheBundle\DependencyInjection\Compiler
*/
class CollectFeedFetchersCompilerPass implements CompilerPassInterface
{
const LAZY_FACTORY_ID = 'cache.feed_fetcher_factory.lazy';
/**
* You can modify the container here before it is dumped to PHP code.
*
* @param ContainerBuilder $container A ContainerBuilder instance.
*
* @return void
*/
public function process(ContainerBuilder $container)
{
if (! $container->has(self::LAZY_FACTORY_ID)) {
$this->throwException('Lazy factory not registered.');
}
$lazyFactory = $container->getDefinition(self::LAZY_FACTORY_ID);
if ($lazyFactory->getClass() !== LazyFeedFetcherFactory::class) {
$this->throwException(
'Invalid factory, expected '. LazyFeedFetcherFactory::class
.' but got '. $lazyFactory->getClass()
);
}
$fetchers = array_keys($container->findTaggedServiceIds('socialhose.feed_fetcher'));
$map = [];
foreach ($fetchers as $id) {
$class = $container->getDefinition($id)->getClass();
$reflection = new \ReflectionClass($class);
if (! $reflection->implementsInterface(FeedFetcherInterface::class)) {
$this->throwException('');
}
$map[$class::support()] = $id;
}
$lazyFactory->replaceArgument(1, $map);
}
/**
* @param string $message A additional exception message.
*
* @return void
*/
private function throwException($message = '')
{
throw new \RuntimeException('Can\'t register feed fetchers in lazy factory. '. $message);
}
}
@@ -0,0 +1,271 @@
<?php
namespace CacheBundle\Document\Extractor;
use AppBundle\Enum\UnhandledEnumException;
use UserBundle\Enum\ThemeOptionExtractEnum;
/**
* Class BasicDocumentContentExtractor
*
* @package CacheBundle\Document\Extractor
*/
class BasicDocumentContentExtractor implements DocumentContentExtractorInterface
{
/**
* Words and symbols which should be ignored.
*
* @var string[]
*/
private static $unnecessaryWords = [
'AND',
'OR',
'NOT',
'(',
')',
'+',
'*',
'-',
'\\',
'//',
];
/**
* Regexp for unnecessary parts of query.
*
* @var string[]
*/
private static $unnecessaryRegexp = [
'/\^\d+?/',
'/~\d+?/',
];
/**
* @var integer
*/
private $startExtractLen;
/**
* @var integer
*/
private $contextExtractLen;
/**
* @var \Closure[]
*/
private $converters = [];
/**
* @var array[]
*/
private $keywordsCache = [];
/**
* BasicDocumentContentExtractor constructor.
*
* @param integer $startExtractLen How many symbols extract for 'start'
* extract type.
* @param integer $contextExtractLen How many symbols extract before and after
* keyword. Used for 'context' extract type.
*/
public function __construct($startExtractLen, $contextExtractLen)
{
$this->startExtractLen = $startExtractLen;
$this->contextExtractLen = $contextExtractLen;
}
/**
* @param string $content The document contents.
* @param string $query Search query.
* @param ThemeOptionExtractEnum $extract Extract type.
* @param boolean $highlight Should highlight matched keywords
* or not.
*
* @return ExtractionResult
*/
public function extract(
$content,
$query,
ThemeOptionExtractEnum $extract,
$highlight = false
) {
switch ($extract->getValue()) {
//
// We should not extract document content.
//
case ThemeOptionExtractEnum::NO:
$text = '';
$offset = '';
$extractedLength = '';
break;
//
// Extract specific numbers of characters from start of document.
//
case ThemeOptionExtractEnum::START:
$text = mb_substr($content, 0, $this->startExtractLen);
$offset = 0;
$extractedLength = $this->startExtractLen;
break;
//
// Extract specified number of character before and after first
// matched keyword.
//
case ThemeOptionExtractEnum::CONTEXT:
$keywords = $this->splitQueryOnKeywords($query);
list ($offset, $keywordLength) = $this->getNearestKeyword($keywords, $content);
if ($offset === -1) {
//
// We don't find any of search keywords. It maybe when matched
// keyword is found in another document property, like 'title'.
//
// In this case we fallback to 'start' extractor.
//
$text = mb_substr($content, 0, $this->startExtractLen);
$offset = 0;
$extractedLength = $this->startExtractLen;
} else {
//
// Convert current offset into proper UTF value and extract
// text.
//
$converter = $this->createOffsetConverter($content);
$offset = $converter($offset);
//
// Compute start index and length of extract.
//
$extractStart = $offset - $this->contextExtractLen;
$overplus = 0;
if ($extractStart < 0) {
$overplus = abs($extractStart);
$extractStart = 0;
}
$extractedLength = $keywordLength + ($this->contextExtractLen * 2) - $overplus;
$text = mb_substr($content, $extractStart, $extractedLength, 'UTF-8');
}
break;
default:
throw UnhandledEnumException::fromInstance($extract);
}
if ($highlight) {
// termporarily disable highlighting because of how it appears in emails
// ~me 20200425
// $text = $this->highlight($text, $query);
}
return new ExtractionResult($text, $offset, $extractedLength);
}
/**
* @param string $query Search query.
*
* @return array
*/
private function splitQueryOnKeywords($query)
{
//
// Cache all splitted keywords in order to speedup processing.
//
if (! isset($this->keywordsCache[$query])) {
$query = str_replace(self::$unnecessaryWords, '', $query);
$query = preg_replace(self::$unnecessaryRegexp, '', $query);
$this->keywordsCache[$query] = array_filter(\nspl\a\map('trim', mb_split(' ', $query)));
}
return $this->keywordsCache[$query];
}
/**
* @param array $keywords Array of keywords.
* @param string $content A ArticleDocument content.
*
* @return array
*/
private function getNearestKeyword(array $keywords, $content)
{
$offset = -1;
$keywordLength = 0;
foreach ($keywords as $keyword) {
$matched = [];
preg_match('/(' . $keyword . ')/i', $content, $matched, PREG_OFFSET_CAPTURE);
if (isset($matched[0]) && (($offset === -1) || ($offset > $matched[0][1]))) {
$offset = $matched[0][1];
$keywordLength = mb_strlen($matched[0][0], 'UTF-8');
}
}
return [ $offset, $keywordLength ];
}
/**
* @param string $content Document content.
*
* @return \Closure
*/
private function createOffsetConverter($content)
{
//
// Save all created converters in order to speedup processing.
//
$hash = sha1($content);
if (! isset($this->converters[$hash])) {
$contentLength = mb_strlen($content);
$utfMap = [];
for ($offset = 0; $offset < $contentLength; $offset++) {
//
// Single unicode character in ANSI format may have one and more
// 'characters' (character codes). So for proper offset computation
// we should get current character, compute it length in ANSI format
// and create proper map between ANSI offset and Unicode offset.
//
$char = mb_substr($content, $offset, 1);
$nonUtfLength = strlen($char);
for ($charOffset = 0; $charOffset < $nonUtfLength; $charOffset++) {
$utfMap[] = $offset;
}
}
$this->converters[$hash] = static function ($offset) use ($utfMap) {
return $utfMap[$offset];
};
}
return $this->converters[$hash];
}
/**
* @param string $text Highlighted text.
* @param string $query Query.
*
* @return string
*/
private function highlight($text, $query)
{
$keywords = $this->splitQueryOnKeywords($query);
foreach ($keywords as $keyword) {
// this is a very dumb line, but somewhere there is an issue with highlighting
// where when "in' is present in a string, it only highlights it. E.g. if a search
// string is: "building in public" the result is that it only highlights the word
// "in".
// [Need a real fix]
if (!preg_match("/\bin\b/i", $keyword)) {
$text = preg_replace('/('. $keyword .')/i', '<span class=\'cw-keyword--highlight\'>$1</span>', $text);
}
}
return $text;
}
}
@@ -0,0 +1,30 @@
<?php
namespace CacheBundle\Document\Extractor;
use UserBundle\Enum\ThemeOptionExtractEnum;
/**
* Interface DocumentContentExtractorInterface
*
* @package CacheBundle\Document\Extractor
*/
interface DocumentContentExtractorInterface
{
/**
* @param string $content The document contents.
* @param string $query Search query.
* @param ThemeOptionExtractEnum $extract Extract type.
* @param boolean $highlight Should highlight matched keywords
* or not.
*
* @return ExtractionResult
*/
public function extract(
$content,
$query,
ThemeOptionExtractEnum $extract,
$highlight = false
);
}
@@ -0,0 +1,65 @@
<?php
namespace CacheBundle\Document\Extractor;
/**
* Class ExtractionResult
*
* @package CacheBundle\Document\Extractor
*/
class ExtractionResult
{
/**
* @var string
*/
private $text;
/**
* @var integer
*/
private $start;
/**
* @var integer
*/
private $length;
/**
* ExtractionResult constructor.
*
* @param string $text Extracted text.
* @param integer $start The position with which the extraction began.
* @param integer $length How much extracts.
*/
public function __construct($text, $start, $length)
{
$this->text = $text;
$this->start = $start;
$this->length = $length;
}
/**
* @return string
*/
public function getText()
{
return $this->text;
}
/**
* @return integer
*/
public function getStart()
{
return $this->start;
}
/**
* @return integer
*/
public function getLength()
{
return $this->length;
}
}
@@ -0,0 +1,248 @@
<?php
namespace CacheBundle\Entity\Analytic;
use ApiBundle\Entity\ManageableEntityInterface;
use ApiBundle\Entity\NormalizableEntityInterface;
use ApiBundle\Serializer\Metadata\Metadata;
use ApiBundle\Serializer\Metadata\PropertyMetadata;
use AppBundle\Entity\BaseEntityTrait;
use CacheBundle\Form\AnalyticType;
use Doctrine\ORM\Mapping as ORM;
use UserBundle\Entity\User;
/**
* Analytic
*
* Holds all data necessary for viewing and computing analytics.
*
* @ORM\Table(name="analytics")
* @ORM\Entity(
* repositoryClass="CacheBundle\Repository\AnalyticRepository"
* )
*
* @see Analytic
* @ORM\HasLifecycleCallbacks
*/
class Analytic implements
ManageableEntityInterface,
NormalizableEntityInterface
{
use BaseEntityTrait;
/**
* @var string
*
* @ORM\Column(nullable=true)
*/
private $name;
/**
* @var User
*
* @ORM\ManyToOne(targetEntity="UserBundle\Entity\User")
*/
private $owner;
/**
* @var AnalyticContext
*
* @ORM\ManyToOne(
* targetEntity="CacheBundle\Entity\Analytic\AnalyticContext",fetch="EAGER",
* cascade={ "persist" },
* inversedBy="analytics"
* )
* @ORM\JoinColumn(name="context_id",referencedColumnName="hash")
*/
private $context;
/**
* @var \DateTime
*
* @ORM\Column(type="datetime")
*/
protected $createdAt;
/**
* @var \DateTime
*
* @ORM\Column(type="datetime", nullable=true)
*/
protected $updatedAt;
/**
* Analytic constructor.
*
* @param User $owner Owner of this analytic.
* @param AnalyticContext $context Used context.
*/
public function __construct(User $owner, AnalyticContext $context)
{
$this->owner = $owner;
$this->context = $context;
$this->createdAt = new \DateTime();
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $name A saved analytic name.
*
* @return $this
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* @return User
*/
public function getOwner()
{
return $this->owner;
}
/**
* @param User $owner Saved analytic owner.
*
* @return $this
*/
public function setOwner(User $owner = null)
{
$this->owner = $owner;
return $this;
}
/**
* @return AnalyticContext
*/
public function getContext()
{
return $this->context;
}
/**
* @param AnalyticContext $context Used context.
*
* @return $this
*/
public function setContext(AnalyticContext $context)
{
$this->context = $context;
return $this;
}
/**
* Return metadata for current entity.
*
* @return \ApiBundle\Serializer\Metadata\Metadata
*/
public function getMetadata()
{
return new Metadata(static::class, [
PropertyMetadata::createInteger('id', [ 'id' ]),
PropertyMetadata::createDate('createdAt', [ 'analytic' ]),
PropertyMetadata::createDate('updatedAt', [ 'analytic' ]),
PropertyMetadata::createEntity('context',AnalyticContext::class,['context']),
]);
}
/**
* Return default normalization groups.
*
* @return array
*/
public function defaultGroups()
{
return [ 'id', 'analytic','context'];
}
/**
* Return fqcn of form used for creating this entity.
*
* @return string
*/
public function getCreateFormClass()
{
return AnalyticType::class;
}
/**
* Return fqcn of form used for updating this entity.
*
* @return string
*/
public function getUpdateFormClass()
{
return AnalyticType::class;
}
/**
* Check whether specified user owner.
*
* @param User $user A User entity instance.
*
* @return boolean
*/
public function isOwnedBy(User $user)
{
return $user->getId() === $this->owner->getId();
}
/**
* Get createdAt
*
* @return \DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* @ORM\PreUpdate()
*
* @return void
* @throws \Exception
*/
public function onUpdate()
{
$this->setUpdatedAt(new \DateTime());
}
/**
* Set updatedAt
*
* @param \DateTime $updatedAt When this analytic is updated.
*
* @return $Analytic
*/
public function setUpdatedAt(\DateTime $updatedAt = null)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* Get updatedAt
*
* @return \DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
}
@@ -0,0 +1,274 @@
<?php
namespace CacheBundle\Entity\Analytic;
use ApiBundle\Entity\NormalizableEntityInterface;
use ApiBundle\Serializer\Metadata\Metadata;
use ApiBundle\Serializer\Metadata\PropertyMetadata;
use AppBundle\Entity\EntityInterface;
use CacheBundle\Entity\Feed\AbstractFeed;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use IndexBundle\Filter\FilterInterface;
/**
* AnalyticContext
*
* Holds all necessary data for making analytic computation.
*
* Created 'cause we may got same analytic request from different users, so we
* should'nt create two equals analytic.
*
* @ORM\Table(name="analytics_context")
* @ORM\Entity
*/
class AnalyticContext implements
EntityInterface,
NormalizableEntityInterface
{
/**
* @var string
*
* @ORM\Column
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
*/
private $hash;
/**
* @var Collection
*
* @ORM\ManyToMany(targetEntity="CacheBundle\Entity\Feed\AbstractFeed")
* @ORM\JoinTable(
* name="cross_analytics_feeds",
* joinColumns={@ORM\JoinColumn(referencedColumnName="hash",onDelete="CASCADE")},
* inverseJoinColumns={@ORM\JoinColumn(name="feed_id")}
* )
*/
private $feeds;
/**
* Array of normalized and actually used filters.
*
* @var array
*
* @ORM\Column(type="array")
*/
private $filters;
/**
* Filters in the form in which they came to us from the client.
*
* @var array
*
* @ORM\Column(type="json_array")
*/
private $rawFilters;
/**
* @ORM\OneToMany(targetEntity="CacheBundle\Entity\Analytic\Analytic", mappedBy="context")
* @ORM\JoinColumn(nullable=false)
*/
private $analytics;
/**
* AnalyticContext constructor.
*
* @param string $hash Internal analytic hash used for determining
* uniqueness of analytic request.
* @param array $feeds Feeds used as source for analytic.
* @param array $filters Additional filters applying on data from feeds.
* @param array $rawFilters Filters as is it passed from frontend.
*/
public function __construct(
$hash,
array $feeds,
array $filters = [],
array $rawFilters = []
) {
if (! \app\a\allInstanceOf($feeds, AbstractFeed::class)) {
throw new \InvalidArgumentException(sprintf(
'\'$feeds\' should be an array of \'%s\'',
AbstractFeed::class
));
}
if (! \app\a\allInstanceOf($filters, FilterInterface::class)) {
throw new \InvalidArgumentException(sprintf(
'\'$filters\' should be an array of \'%s\'',
FilterInterface::class
));
}
$this->hash = $hash;
$this->feeds = new ArrayCollection($feeds);
$this->filters = $filters;
$this->rawFilters = $rawFilters;
}
/**
* Get id
*
* @return mixed
*/
public function getId()
{
return $this->hash;
}
/**
* @return string
*/
public function getHash()
{
return $this->hash;
}
/**
* @param string $hash Analytic hash.
*
* @return $this
*/
public function setHash($hash)
{
$this->hash = $hash;
return $this;
}
/**
* Get entity type
*
* @return string
*/
public function getEntityType()
{
return 'analytic';
}
/**
* @param AbstractFeed $feed A added feed.
*
* @return $this
*/
public function addFeed(AbstractFeed $feed)
{
$this->feeds[] = $feed;
return $this;
}
/**
* @param AbstractFeed $feed A removed feed.
*
* @return $this
*/
public function removeFeed(AbstractFeed $feed)
{
$this->feeds->removeElement($feed);
return $this;
}
/**
* @return Collection
*/
public function getFeeds()
{
return $this->feeds;
}
/**
* @return array
*/
public function getFilters()
{
return $this->filters;
}
/**
* @param array $filters Array of normalized filters.
*
* @return $this
*/
public function setFilters(array $filters)
{
$this->filters = $filters;
return $this;
}
/**
* @return array
*/
public function getRawFilters()
{
return $this->rawFilters;
}
/**
* @param array $rawFilters Array of filters as is.
*
* @return $this
*/
public function setRawFilters(array $rawFilters)
{
$this->rawFilters = $rawFilters;
return $this;
}
/**
* @return mixed
*/
public function getAnalytics()
{
return $this->analytics;
}
/**
* @param mixed $analytics
*/
public function setAnalytics($analytics): void
{
$this->analytics = $analytics;
}
/**
* Return metadata for current entity.
*
* @return Metadata
*/
public function getMetadata()
{
return new Metadata(static::class, [
PropertyMetadata::createString('hash', [ 'context' ]),
PropertyMetadata::createObject('filters', [ 'context' ]),
PropertyMetadata::createArray('rawFilters', [ 'context' ]),
PropertyMetadata::createArray('feeds', [ 'context' ])
->setField(function () {
$feeds = $this->feeds->map(function (AbstractFeed $feed) {
return [
'id' => $feed->getId(),
'name' => $feed->getName(),
];
})->toArray();
return $feeds;
}),
]);
}
/**
* Return default normalization groups.
*
* @return array
*/
public function defaultGroups()
{
return [ 'context'];
}
}
+511
View File
@@ -0,0 +1,511 @@
<?php
namespace CacheBundle\Entity;
use ApiBundle\Entity\ManageableEntityInterface;
use ApiBundle\Entity\NormalizableEntityInterface;
use ApiBundle\Serializer\Metadata\Metadata;
use ApiBundle\Serializer\Metadata\PropertyMetadata;
use AppBundle\Entity\BaseEntityTrait;
use CacheBundle\Form\CategoryType;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Validator\Constraints\CategoryParent;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use UserBundle\Entity\User;
/**
* Category
*
* @ORM\Table(name="categories")
* @ORM\Entity(
* repositoryClass="CacheBundle\Repository\CategoryRepository"
* )
*
* @Assert\GroupSequence({ "Category", "parent" , "unique" })
*/
class Category implements
ManageableEntityInterface,
NormalizableEntityInterface
{
use BaseEntityTrait;
/**
* My category.
*/
const TYPE_MY_CONTENT = 'my_content';
/**
* My category name.
*/
const NAME_MY_CONTENT = 'My Content';
/**
* Category for deleted feeds.
*/
const TYPE_DELETED_CONTENT = 'deleted_content';
/**
* Name of category for deleted feeds.
*/
const NAME_DELETED_CONTENT = 'Deleted Content';
/**
* Category for shared feeds.
*/
const TYPE_SHARED_CONTENT = 'shared_content';
/**
* Name of category for shared feeds.
*/
const NAME_SHARED_CONTENT = 'Shared Content';
/**
* User custom directories.
*/
const TYPE_CUSTOM = 'custom';
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\OneToMany(
* targetEntity="CacheBundle\Entity\Feed\AbstractFeed",
* mappedBy="category",
* cascade={ "persist", "remove" }
* )
*/
protected $feeds;
/**
* @var User
*
* @ORM\ManyToOne(
* targetEntity="UserBundle\Entity\User",
* inversedBy="categories"
* )
*/
protected $user;
/**
* @var string
*
* @ORM\Column
*
* @Assert\NotBlank
*/
protected $name;
/**
* @var string
*
* @ORM\Column
*/
protected $type = self::TYPE_CUSTOM;
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\OneToMany(
* targetEntity="CacheBundle\Entity\Category",
* mappedBy="parent",
* cascade={ "persist", "remove" }
* )
*/
protected $childes;
/**
* @var Category
*
* @ORM\ManyToOne(
* targetEntity="CacheBundle\Entity\Category",
* inversedBy="childes"
* )
* @CategoryParent(groups={ "parent" })
*/
protected $parent;
/**
* @var boolean
*
* @ORM\Column(type="boolean")
*/
protected $exported = false;
/**
* @param User $user A User entity instance, who create this category.
* @param string $name Category name.
*/
public function __construct(User $user, $name = '')
{
$user->addCategory($this);
$this->name = $name;
$this->feeds = new ArrayCollection();
$this->childes = new ArrayCollection();
}
/**
* @param Category $parent A parent Category entity instance.
* @param User $user A User entity instance, who create this category.
* @param string $name Category name.
*
* @return Category
*/
public static function createChild(Category $parent, User $user, $name)
{
$category = new Category($user, $name);
$parent->addChild($category);
return $category;
}
/**
* Create main category for specified user.
*
* @param User $user A User entity instance.
*
* @return Category
*/
public static function createMainCategory(User $user)
{
$category = new Category($user, self::NAME_MY_CONTENT);
return $category->setType(self::TYPE_MY_CONTENT);
}
/**
* Create main category for specified user.
*
* @param User $user A User entity instance.
*
* @return Category
*/
public static function createSharedCategory(User $user)
{
$category = new Category($user, self::NAME_SHARED_CONTENT);
return $category->setType(self::TYPE_SHARED_CONTENT);
}
/**
* Create trash category for specified user.
*
* @param User $user A User entity instance.
*
* @return Category
*/
public static function createTrashCategory(User $user)
{
$category = new Category($user, self::NAME_DELETED_CONTENT);
return $category->setType(self::TYPE_DELETED_CONTENT);
}
/**
* Add query
*
* @param AbstractFeed $feed A AbstractFeed entity instance.
*
* @return Category
*/
public function addFeed(AbstractFeed $feed)
{
$this->feeds[] = $feed;
$feed->setCategory($this);
return $this;
}
/**
* Remove query
*
* @param AbstractFeed $feed A AbstractFeed entity instance.
*
* @return Category
*/
public function removeFeed(AbstractFeed $feed)
{
$this->feeds->removeElement($feed);
$feed->setCategory(null);
return $this;
}
/**
* Get queries
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getFeeds()
{
return $this->feeds;
}
/**
* Set user
*
* @param User $user A User entity instance.
*
* @return static
*/
public function setUser(User $user = null)
{
$this->user = $user;
return $this;
}
/**
* Get user
*
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* Check whether specified user owner.
*
* @param User $user A User entity instance.
*
* @return boolean
*/
public function isOwnedBy(User $user)
{
return $user->getId() === $this->user->getId();
}
/**
* Set name
*
* @param string $name Category name.
*
* @return Category
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set type
*
* @param string $type Category type.
*
* @return Category
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* Get type
*
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* Return true if current category is my content, deleted content or shared
* content.
*
* @return boolean
*/
public function isInternal()
{
return $this->type !== self::TYPE_CUSTOM;
}
/**
* Return fqcn of form used for creating this entity.
*
* @return string
*/
public function getCreateFormClass()
{
return CategoryType::class;
}
/**
* Return fqcn of form used for updating this entity.
*
* @return string
*/
public function getUpdateFormClass()
{
return CategoryType::class;
}
/**
* Add child
*
* @param Category $child A child Category entity instance.
*
* @return Category
*/
public function addChild(Category $child)
{
if ($child === $this) {
$message = 'Try to put category inside itself.';
throw new \InvalidArgumentException($message);
}
$this->childes[] = $child;
$child->setParent($this);
return $this;
}
/**
* Remove child
*
* @param Category $child A child Category entity instance.
*
* @return Category
*/
public function removeChild(Category $child)
{
$this->childes->removeElement($child);
$child->setParent(null);
return $this;
}
/**
* Get childs
*
* @return \Doctrine\Common\Collections\Collection|array
*/
public function getChildes()
{
return $this->childes;
}
/**
* Set parent
*
* @param Category $parent A parent Category entity instance.
*
* @return Category
*/
public function setParent(Category $parent = null)
{
$this->parent = $parent;
return $this;
}
/**
* Get parent
*
* @return Category
*/
public function getParent()
{
return $this->parent;
}
/**
* @return boolean
*/
public function isExported()
{
return $this->exported;
}
/**
* @param boolean $exported Is this feed exported or not.
*
* @return static
*/
public function setExported($exported)
{
$this->exported = $exported;
return $this;
}
/**
* Return metadata for current entity.
*
* @return Metadata
*/
public function getMetadata()
{
return new Metadata(static::class, [
PropertyMetadata::createInteger('id', [ 'id' ]),
PropertyMetadata::createString('name', [ 'category', 'category_tree' ]),
PropertyMetadata::createString('subType', [ 'category', 'category_tree' ])
->setField('type'),
PropertyMetadata::createCollection('childes', Category::class, [
'category',
'category_tree',
]),
PropertyMetadata::createBoolean('exported', [ 'category', 'category_tree' ]),
PropertyMetadata::createCollection('feeds', AbstractFeed::class, [
'feed_tree',
]),
]);
}
/**
* Return default normalization groups.
*
* @return array
*/
public function defaultGroups()
{
return [ 'feed_tree', 'category', 'id' ];
}
/**
* Get entity type
*
* @return string
*/
public function getEntityType()
{
return 'directory';
}
/**
* @Assert\Callback(groups={ "unique" })
*
* @param ExecutionContextInterface $context A ExecutionContextInterface instance.
*/
public function validateUnique(ExecutionContextInterface $context)
{
$categoriesWithSameName = $this->getParent()->getChildes()->filter(function (Category $category) {
return $category->getName() === $this->getName();
});
if (count($categoriesWithSameName) > 0) {
$context->buildViolation('Category with name \'{{ value }}\' is already exists')
->setParameter('{{ value }}', $this->getName())
->addViolation();
}
}
}
+282
View File
@@ -0,0 +1,282 @@
<?php
namespace CacheBundle\Entity;
use ApiBundle\Entity\ManageableEntityInterface;
use ApiBundle\Entity\NormalizableEntityInterface;
use ApiBundle\Serializer\Metadata\Metadata;
use ApiBundle\Serializer\Metadata\PropertyMetadata;
use AppBundle\Entity\BaseEntityTrait;
use AppBundle\Entity\EntityInterface;
use CacheBundle\Form\CommentType;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use UserBundle\Entity\User;
/**
* Comment
*
* @ORM\Table(name="comments")
* @ORM\Entity(repositoryClass="CacheBundle\Repository\CommentRepository")
*/
class Comment implements EntityInterface, NormalizableEntityInterface, ManageableEntityInterface
{
use BaseEntityTrait;
/**
* @var Document
*
* @ORM\ManyToOne(targetEntity="CacheBundle\Entity\Document", inversedBy="comments")
*/
private $document;
/**
* @var User
*
* @ORM\ManyToOne(targetEntity="UserBundle\Entity\User")
*/
private $author;
/**
* @var string
*
* @ORM\Column
*/
private $title = '';
/**
* @var string
*
* @ORM\Column(type="text")
*
* @Assert\NotBlank
* @Assert\Length(max=5000)
*/
private $content;
/**
* @var \DateTime
*
* @ORM\Column(type="datetime")
*/
private $createdAt;
/**
* Only last of specified number of comments is marked as 'new'.
* We use this marker for simplify fetching comments while we get list of
* document.
*
* @var boolean
*
* @ORM\Column(type="boolean")
*/
private $new = true;
/**
* Comment constructor.
*
* @param User $user A comment author.
* @param string $content Comment content.
* @param string $title Comment title.
*/
public function __construct(User $user, $content, $title = '')
{
$this
->setAuthor($user)
->setContent($content)
->setTitle($title);
$this->createdAt = new \DateTime();
}
/**
* Set title
*
* @param string $title Comment title.
*
* @return Comment
*/
public function setTitle($title)
{
$this->title = trim($title);
return $this;
}
/**
* Get title
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Set content
*
* @param string $content Comment content.
*
* @return Comment
*/
public function setContent($content)
{
$this->content = trim($content);
return $this;
}
/**
* Get content
*
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* Set createdAt
*
* @param \DateTime $createdAt When comment was created.
*
* @return Comment
*/
public function setCreatedAt(\DateTime $createdAt = null)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Get createdAt
*
* @return \DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Set document
*
* @param Document $document A Document entity instance.
*
* @return Comment
*/
public function setDocument(Document $document = null)
{
$this->document = $document;
return $this;
}
/**
* Get document
*
* @return Document
*/
public function getDocument()
{
return $this->document;
}
/**
* Set author
*
* @param User $author A User entity instance.
*
* @return Comment
*/
public function setAuthor(User $author = null)
{
$this->author = $author;
return $this;
}
/**
* Get author
*
* @return User
*/
public function getAuthor()
{
return $this->author;
}
/**
* Set new
*
* @param boolean $new Flag, true if this comment is new.
*
* @return Comment
*/
public function setNew($new = true)
{
$this->new = $new;
return $this;
}
/**
* Is new
*
* @return boolean
*/
public function isNew()
{
return $this->new;
}
/**
* Return metadata for current entity.
*
* @return \ApiBundle\Serializer\Metadata\Metadata
*/
public function getMetadata()
{
return new Metadata(static::class, [
PropertyMetadata::createInteger('id', [ 'id' ]),
PropertyMetadata::createString('title', [ 'comment' ]),
PropertyMetadata::createString('content', [ 'comment' ]),
PropertyMetadata::createEntity('author', User::class, [ 'comment' ]),
PropertyMetadata::createDate('createdAt', [ 'comment' ]),
]);
}
/**
* Return default normalization groups.
*
* @return array
*/
public function defaultGroups()
{
return [ 'id', 'comment' ];
}
/**
* Return fqcn of form used for creating this entity.
*
* @return string
*/
public function getCreateFormClass()
{
return CommentType::class;
}
/**
* Return fqcn of form used for updating this entity.
*
* @return string
*/
public function getUpdateFormClass()
{
return CommentType::class;
}
}
+383
View File
@@ -0,0 +1,383 @@
<?php
namespace CacheBundle\Entity;
use ApiBundle\Entity\NormalizableEntityInterface;
use ApiBundle\Serializer\Metadata\Metadata;
use ApiBundle\Serializer\Metadata\PropertyMetadata;
use AppBundle\Entity\EntityInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Document
*
* @ORM\Table(name="documents")
* @ORM\Entity(repositoryClass="CacheBundle\Repository\DocumentRepository")
*/
class Document implements EntityInterface, NormalizableEntityInterface
{
/**
* @var string
*
* @ORM\Column(type="string")
* @ORM\Id()
* @ORM\GeneratedValue(strategy="NONE")
*/
protected $id;
/**
* @var string
*
* @ORM\Column(type="string")
*/
protected $platform;
/**
* @var array
*
* @ORM\Column(type="json_array")
*/
protected $data;
/**
* @var Collection
*
* @ORM\OneToMany(targetEntity="CacheBundle\Entity\Page", mappedBy="document")
*/
protected $pages;
/**
* @var Collection
*
* @ORM\OneToMany(targetEntity="CacheBundle\Entity\Comment", mappedBy="document")
*/
protected $comments;
/**
* @var integer
*
* @ORM\Column(type="integer")
*/
protected $commentsCount = 0;
/**
* Map Document entity fields to external document fields.
*
* Contains only fields which has different's.
*
* @var string[]
*/
protected static $externalMap = [
'mainLength' => 'main_length',
'dateFound' => 'date_found',
'sourceHashcode' => 'source_hashcode',
'sourceLink' => 'source_link',
'sourcePublisherType' => 'source_publisher_type',
'sourcePublisherSubtype' => 'source_publisher_subtype',
'sourceDateFound' => 'source_date_found',
'sourceTitle' => 'source_title',
'sourceDescription' => 'source_description',
'sourceLocation' => 'source_location',
'summaryText' => 'summary_text',
'htmlLength' => 'html_length',
'authorName' => 'author_name',
'authorLink' => 'author_link',
'authorGender' => 'author_gender',
'imageSrc' => 'image_src',
'country' => 'geo_country',
'state' => 'geo_state',
'city' => 'geo_city',
'point' => 'geo_point',
'duplicatesCount' => 'duplicates_count',
];
/**
* Constructor
*/
public function __construct()
{
$this->pages = new ArrayCollection();
$this->comments = new ArrayCollection();
}
/**
* @return static
*/
public static function create()
{
return new static();
}
/**
* Get id
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* @param string $id New document id.
*
* @return Document
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* @return string
*/
public function getPlatform()
{
return $this->platform;
}
/**
* @param string $platform Platform name from which we get document.
*
* @return Document
*/
public function setPlatform($platform)
{
$this->platform = $platform;
return $this;
}
/**
* @return array
*/
public function getData()
{
return $this->data;
}
/**
* @param array $data A index document data.
*
* @return Document
*/
public function setData(array $data)
{
$this->data = $data;
return $this;
}
/**
* Add page
*
* @param Page $page A Page entity instance.
*
* @return Document
*/
public function addPage(Page $page)
{
$this->pages[] = $page;
$page->setDocument($this);
return $this;
}
/**
* Remove page
*
* @param Page $page A Page entity instance.
*
* @return Document
*/
public function removePage(Page $page)
{
$this->pages->removeElement($page);
$page->setDocument(null);
return $this;
}
/**
* Get pages
*
* @return Collection
*/
public function getPages()
{
return $this->pages;
}
/**
* Get entity type
*
* @return string
*/
public function getEntityType()
{
return 'document';
}
/**
* Add comment
*
* @param Comment $comment A Comment entity instance.
*
* @return Document
*/
public function addComment(Comment $comment)
{
$this->comments[] = $comment;
$comment->setDocument($this);
return $this;
}
/**
* Remove comment
*
* @param Comment $comment A Comment entity instance.
*
* @return Document
*/
public function removeComment(Comment $comment)
{
$this->comments->removeElement($comment);
$comment->setDocument(null);
return $this;
}
/**
* Set comments
*
* @param Comment[]|ArrayCollection $comments Array of Document entities.
*
* @return Document
*/
public function setComments($comments)
{
if (is_array($comments)) {
$comments = new ArrayCollection($comments);
}
$this->comments = $comments;
return $this;
}
/**
* Get comments
*
* @return Collection
*/
public function getComments()
{
return $this->comments;
}
/**
* Set commentsCount
*
* @param integer $count Comments count.
*
* @return Document
*/
public function setCommentsCount($count)
{
$this->commentsCount = $count;
return $this;
}
/**
* Get commentsCount
*
* @return integer
*/
public function getCommentsCount()
{
return $this->commentsCount;
}
/**
* Increment comments counts for this document
*
* @return Document
*/
public function incCommentsCount()
{
$this->commentsCount++;
return $this;
}
/**
* Decrement comments counts for this document
*
* @return Document
*/
public function decCommentsCount()
{
$this->commentsCount--;
return $this;
}
/**
* Return metadata for current entity.
*
* @return \ApiBundle\Serializer\Metadata\Metadata
*/
public function getMetadata()
{
return new Metadata(static::class, [
PropertyMetadata::createString('id', [ 'id' ]),
PropertyMetadata::createString('title', [ 'document' ]),
PropertyMetadata::createDate('dateFound', [ 'document' ]),
PropertyMetadata::createDate('published', [ 'document' ]),
PropertyMetadata::createString('permalink', [ 'document' ]),
PropertyMetadata::createString('content', [ 'document' ]),
PropertyMetadata::createString('language', [ 'document' ]),
PropertyMetadata::createString('publisher', [ 'document' ]),
PropertyMetadata::groupProperties('source', [
PropertyMetadata::createString('title', [ 'document' ]),
PropertyMetadata::createString('type', [ 'document' ]),
PropertyMetadata::createString('link', [ 'document' ]),
PropertyMetadata::createString('section', [ 'document' ]),
PropertyMetadata::createString('country', [ 'document' ]),
PropertyMetadata::createString('state', [ 'document' ]),
PropertyMetadata::createString('city', [ 'document' ]),
], [ 'document' ]),
PropertyMetadata::groupProperties('author', [
PropertyMetadata::createString('name', [ 'document' ]),
PropertyMetadata::createString('link', [ 'document' ]),
], [ 'document' ]),
PropertyMetadata::createInteger('duplicates', [ 'document' ]),
PropertyMetadata::createString('image', [ 'document' ])->setNullable(true),
PropertyMetadata::createInteger('views', [ 'document' ]),
PropertyMetadata::createString('sentiment', [ 'document' ]),
PropertyMetadata::groupProperties('comments', [
PropertyMetadata::createCollection('comments', Comment::class, [ 'document' ])
->setName('data'),
PropertyMetadata::createInteger('count', [ 'document' ]),
PropertyMetadata::createInteger('limit', [ 'document' ]),
], [ 'document' ]),
]);
}
/**
* Return default normalization groups.
*
* @return array
*/
public function defaultGroups()
{
return [ 'id', 'document' ];
}
}
@@ -0,0 +1,52 @@
<?php
namespace CacheBundle\Entity;
use Common\Enum\CollectionTypeEnum;
/**
* Interface DocumentCollectionInterface
*
* Used for classes which can be used for holding document collection.
*
* @package CacheBundle\Entity
*/
interface DocumentCollectionInterface
{
/**
* Set totalCount
*
* @param integer $totalCount Count of all available data.
*
* @return static
*/
public function setTotalCount($totalCount);
/**
* Get totalCount
*
* @return integer
*/
public function getTotalCount();
/**
* Create proper Page entity instance for binding document and current
* collection.
*
* @param integer $number Page number.
*
* @return Page
*/
public function createPage($number);
/**
* @return integer
*/
public function getCollectionId();
/**
* @return CollectionTypeEnum
*/
public function getCollectionType();
}
@@ -0,0 +1,42 @@
<?php
namespace CacheBundle\Entity;
/**
* Trait DocumentCollectionInterface
* @package CacheBundle\Entity
*/
trait DocumentCollectionTrait
{
/**
* @var integer
*
* @ORM\Column(type="integer")
*/
protected $totalCount = 0;
/**
* Set totalCount
*
* @param integer $totalCount Count of all available data.
*
* @return static
*/
public function setTotalCount($totalCount)
{
$this->totalCount = $totalCount;
return $this;
}
/**
* Get totalCount
*
* @return integer
*/
public function getTotalCount()
{
return $this->totalCount;
}
}
@@ -0,0 +1,327 @@
<?php
namespace CacheBundle\Entity\Feed;
use ApiBundle\Entity\ManageableEntityInterface;
use ApiBundle\Entity\NormalizableEntityInterface;
use AppBundle\Entity\BaseEntityTrait;
use AppBundle\Entity\EntityInterface;
use CacheBundle\Entity\Category;
use CacheBundle\Entity\Document;
use Common\Enum\CollectionTypeEnum;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use UserBundle\Entity\User;
/**
* AbstractFeed
*
* @ORM\Table(name="feeds")
* @ORM\Entity(
* repositoryClass="CacheBundle\Repository\CommonFeedRepository"
* )
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({
* "query"="QueryFeed",
* "clip"="ClipFeed",
* })
*/
abstract class AbstractFeed implements
EntityInterface,
NormalizableEntityInterface,
ManageableEntityInterface
{
use BaseEntityTrait;
/**
* @var string
*
* @ORM\Column
*
* @Assert\NotBlank(groups={ "Feed_Create" })
*/
protected $name;
/**
* @var User
*
* @ORM\ManyToOne(
* targetEntity="UserBundle\Entity\User"
* )
*/
protected $user;
/**
* @var Category
*
* @ORM\ManyToOne(
* targetEntity="CacheBundle\Entity\Category",
* inversedBy="feeds"
* )
*
* @Assert\NotBlank(groups={ "Feed_Create" })
*/
protected $category;
/**
* @var boolean
*
* @ORM\Column(type="boolean")
*/
protected $exported = false;
/**
* @var Collection
*
* @ORM\ManyToMany(targetEntity="CacheBundle\Entity\Document", cascade={ "persist", "remove" })
* @ORM\JoinTable(name="deleted_documents")
*/
protected $excludedDocuments;
/**
* Constructor
*/
public function __construct()
{
$this->excludedDocuments = new ArrayCollection();
}
/**
* Set name
*
* @param string $name Feed name.
*
* @return AbstractFeed
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set user
*
* @param User $user A User entity instance.
*
* @return static
*/
public function setUser(User $user = null)
{
$this->user = $user;
return $this;
}
/**
* Get user
*
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* Check whether specified user owner.
*
* @param User $user A User entity instance.
*
* @return boolean
*/
public function isOwnedBy(User $user)
{
return $user->getId() === $this->user->getId();
}
/**
* Set category
*
* @param Category $category A Category entity instance.
*
* @return AbstractFeed
*/
public function setCategory(Category $category = null)
{
$this->category = $category;
return $this;
}
/**
* Get category
*
* @return Category
*/
public function getCategory()
{
return $this->category;
}
/**
* @return boolean
*/
public function isExported()
{
return $this->exported;
}
/**
* @return boolean
*
* @deprecated
* @see AbstractFeed::isExported()
*/
public function getExported()
{
return $this->exported;
}
/**
* @param boolean $exported Is this feed exported or not.
*
* @return static
*/
public function setExported($exported)
{
$this->exported = $exported;
return $this;
}
/**
* Return fqcn of form used for creating this entity.
*
* @return string
*/
public function getCreateFormClass()
{
// All derived feed's will be created in different ways.
return '';
}
/**
* Return fqcn of form used for updating this entity.
*
* @return string
*/
public function getUpdateFormClass()
{
return null;
}
/**
* Get entity type
*
* @return string
*/
public function getEntityType()
{
// For all feeds we shouldn't return specific types, only 'feed'.
return 'feed';
}
/**
* @param string $subType A feed subtype.
*
* @return AbstractFeed
*/
public static function createBySubType($subType)
{
switch ($subType) {
case ClipFeed::getSubType():
return new ClipFeed();
case QueryFeed::getSubType():
return new QueryFeed();
default:
throw new \InvalidArgumentException('Unknown sub type.');
}
}
/**
* Get concrete feed type.
*
* @param AbstractFeed $feed A AbstractFeed entity instance.
*
* @return string
*/
public static function getSubType(AbstractFeed $feed = null)
{
return \app\op\camelCaseToUnderscore(\app\c\getShortName($feed ?: static::class));
}
/**
* Get specific feed type.
*
* Used by frontend.
*
* @return string
*/
abstract public function getSpecificType();
/**
* @return integer
*/
abstract public function getCollectionId();
/**
* @return CollectionTypeEnum
*/
abstract public function getCollectionType();
/**
* Add excludedDocument
*
* @param Document $excludedDocument Excluded Document entity instance.
*
* @return static
*/
public function addExcludedDocument(Document $excludedDocument)
{
$this->excludedDocuments[] = $excludedDocument;
return $this;
}
/**
* Remove excludedDocument
*
* @param Document $excludedDocument Removed Document entity instance.
*
* @return static
*/
public function removeExcludedDocument(Document $excludedDocument)
{
$this->excludedDocuments->removeElement($excludedDocument);
return $this;
}
/**
* Get excludedDocuments
*
* @return Collection
*/
public function getExcludedDocuments()
{
return $this->excludedDocuments;
}
}
+235
View File
@@ -0,0 +1,235 @@
<?php
namespace CacheBundle\Entity\Feed;
use ApiBundle\Serializer\Metadata\Metadata;
use ApiBundle\Serializer\Metadata\PropertyMetadata;
use CacheBundle\Entity\Category;
use CacheBundle\Entity\DocumentCollectionInterface;
use CacheBundle\Entity\DocumentCollectionTrait;
use CacheBundle\Entity\Page;
use Common\Enum\CollectionTypeEnum;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use UserBundle\Entity\User;
/**
* ClipFeed
* Contains document clipped from another feeds.
*
* @ORM\Entity(repositoryClass="CacheBundle\Repository\ClipFeedRepository")
*/
class ClipFeed extends AbstractFeed implements DocumentCollectionInterface
{
const READ_LATER = 'Read Later';
use DocumentCollectionTrait;
/**
* @var Collection
*
* @ORM\OneToMany(
* targetEntity="CacheBundle\Entity\Page",
* mappedBy="clipFeed",
* cascade={ "remove" }
* )
*/
private $pages;
/**
* Array of normalized actual used filters.
*
* @var array
*
* @ORM\Column(type="array")
*/
protected $filters = [];
/**
* Advanced filters in the form in which they came to us from the client.
*
* @var array
*
* @ORM\Column(type="json_array")
*/
protected $rawFilters = [];
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->pages = new ArrayCollection();
}
/**
* Set filters
*
* @param array $filters Array of filters.
*
* @return static
*/
public function setFilters(array $filters)
{
$this->filters = $filters;
return $this;
}
/**
* Get filters
*
* @return array
*/
public function getFilters()
{
return $this->filters;
}
/**
* Set rawFilters
*
* @param array $rawFilters Raw filters.
*
* @return static
*/
public function setRawFilters(array $rawFilters)
{
$this->rawFilters = $rawFilters;
return $this;
}
/**
* Get rawFilters
*
* @return array
*/
public function getRawFilters()
{
return $this->rawFilters;
}
/**
* Get specific feed type.
*
* Used by frontend.
*
* @return string
*/
public function getSpecificType()
{
return 'feed-type-clippings';
}
/**
* Return metadata for current entity.
*
* @return Metadata
*/
public function getMetadata()
{
return new Metadata(static::class, [
PropertyMetadata::createInteger('id', [ 'id' ]),
PropertyMetadata::createString('name', [ 'feed', 'feed_tree' ]),
PropertyMetadata::createString('subType', [ 'feed', 'feed_tree' ])
->setField(function () {
return static::getSubType();
}),
PropertyMetadata::createString('class', [ 'feed', 'feed_tree' ])
->setField(function () {
return $this->getSpecificType();
}),
PropertyMetadata::createBoolean('exported', [ 'feed', 'feed_tree' ])
->setField(function () {
return $this->getExported();
}),
PropertyMetadata::createEntity('category', Category::class, [ 'feed' ]),
PropertyMetadata::createEntity('user', User::class, [ 'feed' ]),
]);
}
/**
* Return default normalization groups.
*
* @return array
*/
public function defaultGroups()
{
return [ 'feed', 'id' ];
}
/**
* Add page
*
* @param Page $page A page entity instance.
*
* @return ClipFeed
*/
public function addPage(Page $page)
{
$this->pages[] = $page;
$page->setClipFeed($this);
return $this;
}
/**
* Remove page
*
* @param Page $page A Page entity instance.
*
* @return ClipFeed
*/
public function removePage(Page $page)
{
$this->pages->removeElement($page);
$page->setClipFeed(null);
return $this;
}
/**
* Get pages
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getPages()
{
return $this->pages;
}
/**
* Create proper Page entity instance for binding document and current
* collection.
*
* @param integer $number Page number.
*
* @return Page
*/
public function createPage($number)
{
return Page::create()
->setClipFeed($this)
->setNumber($number);
}
/**
* @return integer
*/
public function getCollectionId()
{
return $this->id;
}
/**
* @return CollectionTypeEnum
*/
public function getCollectionType()
{
return CollectionTypeEnum::feed();
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
namespace CacheBundle\Entity\Feed;
use ApiBundle\Serializer\Metadata\Metadata;
use ApiBundle\Serializer\Metadata\PropertyMetadata;
use CacheBundle\Entity\Category;
use CacheBundle\Entity\Query\StoredQuery;
use Common\Enum\CollectionTypeEnum;
use Doctrine\ORM\Mapping as ORM;
use UserBundle\Entity\User;
/**
* QueryFeed
* Feed which create from stored query.
*
* @ORM\Entity(repositoryClass="CacheBundle\Repository\QueryFeedRepository")
*/
class QueryFeed extends AbstractFeed
{
/**
* @var StoredQuery
*
* @ORM\ManyToOne(
* targetEntity="CacheBundle\Entity\Query\StoredQuery",
* inversedBy="feeds"
* )
*/
protected $query;
/**
* @var array
*
* @ORM\Column(type="array")
*/
protected $publisherTypes;
/**
* Set query
*
* @param StoredQuery $query A StoredQuery entity instance.
*
* @return QueryFeed
*/
public function setQuery(StoredQuery $query = null)
{
$this->query = $query;
return $this;
}
/**
* Get query
*
* @return StoredQuery
*/
public function getQuery()
{
return $this->query;
}
/**
* Set publisherTypes
*
* @param array|string $publisherTypes Query publisher type.
*
* @return QueryFeed
*/
public function setPublisherTypes($publisherTypes)
{
$this->publisherTypes = (array) $publisherTypes;
return $this;
}
/**
* Get publisherTypes
*
* @return array
*/
public function getPublisherTypes()
{
return $this->publisherTypes;
}
/**
* Get specific feed type.
*
* Used by frontend.
*
* @return string
*/
public function getSpecificType()
{
if (is_array($this->publisherTypes) && count($this->publisherTypes) === 1) {
return 'feed-type-'
. strtolower(current($this->publisherTypes));
}
return 'feed-type-mixed';
}
/**
* Return metadata for current entity.
*
* @return Metadata
*/
public function getMetadata()
{
return new Metadata(static::class, [
PropertyMetadata::createInteger('id', [ 'id' ]),
PropertyMetadata::createInteger('query', [ 'feed', 'feed_tree' ])
->setField(function () {
return $this->query->getId();
}),
PropertyMetadata::createString('name', [ 'feed', 'feed_tree' ]),
PropertyMetadata::createString('subType', [ 'feed', 'feed_tree' ])
->setField(function () {
return static::getSubType();
}),
PropertyMetadata::createString('class', [ 'feed', 'feed_tree' ])
->setField(function () {
return $this->getSpecificType();
}),
PropertyMetadata::createBoolean('exported', [ 'feed', 'feed_tree' ]),
// ->setField(function () {
// return $this->isExported();
// }),
PropertyMetadata::createEntity('category', Category::class, [ 'feed' ]),
PropertyMetadata::createEntity('user', User::class, [ 'feed' ]),
]);
}
/**
* Return default normalization groups.
*
* @return array
*/
public function defaultGroups()
{
return [ 'feed', 'id' ];
}
/**
* @return integer
*/
public function getCollectionId()
{
return $this->query->getId();
}
/**
* @return CollectionTypeEnum
*/
public function getCollectionType()
{
return CollectionTypeEnum::query();
}
}
+177
View File
@@ -0,0 +1,177 @@
<?php
namespace CacheBundle\Entity;
use AppBundle\Entity\BaseEntityTrait;
use AppBundle\Entity\EntityInterface;
use CacheBundle\Entity\Feed\ClipFeed;
use CacheBundle\Entity\Query\AbstractQuery;
use Common\Enum\CollectionTypeEnum;
use Doctrine\ORM\Mapping as ORM;
/**
* Page
*
* @ORM\Table(name="pages")
* @ORM\Entity
*/
class Page implements EntityInterface
{
use BaseEntityTrait;
/**
* @var AbstractQuery
*
* @ORM\ManyToOne(
* targetEntity="CacheBundle\Entity\Query\AbstractQuery",
* inversedBy="pages"
* )
*/
protected $query;
/**
* @var ClipFeed
*
* @ORM\ManyToOne(
* targetEntity="CacheBundle\Entity\Feed\ClipFeed",
* inversedBy="pages"
* )
*/
protected $clipFeed;
/**
* Page number, starts from 1.
*
* @var integer
*
* @ORM\Column(type="integer")
*/
protected $number;
/**
* @var Document
*
* @ORM\ManyToOne(
* targetEntity="CacheBundle\Entity\Document",
* inversedBy="pages"
* )
*/
protected $document;
/**
* Set number
*
* @param integer $number A page number.
*
* @return Page
*/
public function setNumber($number)
{
$this->number = $number;
return $this;
}
/**
* Get number
*
* @return integer
*/
public function getNumber()
{
return $this->number;
}
/**
* Set query
*
* @param AbstractQuery $query A AbstractQuery entity instance.
*
* @return Page
*/
public function setQuery(AbstractQuery $query = null)
{
$this->query = $query;
return $this;
}
/**
* Get query
*
* @return AbstractQuery
*/
public function getQuery()
{
return $this->query;
}
/**
* Set document
*
* @param Document $document A Document entity instance.
*
* @return Page
*/
public function setDocument(Document $document = null)
{
$this->document = $document;
return $this;
}
/**
* Get document
*
* @return Document
*/
public function getDocument()
{
return $this->document;
}
/**
* Set clipFeed
*
* @param ClipFeed $clipFeed A ClipFeed entity instance.
*
* @return Page
*/
public function setClipFeed(ClipFeed $clipFeed = null)
{
$this->clipFeed = $clipFeed;
return $this;
}
/**
* Get clipFeed
*
* @return ClipFeed
*/
public function getClipFeed()
{
return $this->clipFeed;
}
/**
* Get associated document collection type.
*
* @return string
*/
public function getCollectionType()
{
return $this->query ? CollectionTypeEnum::QUERY : CollectionTypeEnum::FEED;
}
/**
* Get associated document collection entity id.
*
* @return string
*/
public function getCollectionId()
{
return \app\op\invokeIf($this->query, 'getId') ?: $this->clipFeed->getId();
}
}
@@ -0,0 +1,399 @@
<?php
namespace CacheBundle\Entity\Query;
use AppBundle\Entity\BaseEntityTrait;
use AppBundle\Entity\EntityInterface;
use CacheBundle\Entity\DocumentCollectionInterface;
use CacheBundle\Entity\DocumentCollectionTrait;
use CacheBundle\Entity\Page;
use Common\Enum\CollectionTypeEnum;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use IndexBundle\SearchRequest\SearchRequestInterface;
/**
* AbstractQuery
*
* @ORM\Table(
* name="queries",
* indexes={
* @ORM\Index(name="hash_idx", columns={ "hash" })
* }
* )
* @ORM\Entity
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({
* "simple"="SimpleQuery",
* "stored"="StoredQuery"
* })
*/
abstract class AbstractQuery implements EntityInterface, DocumentCollectionInterface
{
use
BaseEntityTrait,
DocumentCollectionTrait;
/**
* @var string
*
* @ORM\Column(type="text")
*/
protected $raw;
/**
* Filters in the form in which they came to us from the client.
*
* @var array
*
* @ORM\Column(type="json_array")
*/
protected $rawFilters = [];
/**
* Advanced filters in the form in which they came to us from the client.
*
* @var array
*
* @ORM\Column(type="json_array")
*/
protected $rawAdvancedFilters = [];
/**
* @var string[]
*
* @ORM\Column(type="array")
*/
protected $fields = [];
/**
* @var string
*
* @ORM\Column(type="text")
*/
protected $normalized;
/**
* @var string
*
* @ORM\Column(type="string")
*/
protected $hash;
/**
* Array of normalized actual used filters.
*
* @var array
*
* @ORM\Column(type="array")
*/
protected $filters = [];
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\OneToMany(targetEntity="CacheBundle\Entity\Page", mappedBy="query")
*/
protected $pages;
/**
* @var string
*
* @ORM\Column(type="datetime")
*/
protected $date;
/**
* Constructor
*/
public function __construct()
{
$this->pages = new ArrayCollection();
$this->date = new \DateTime();
}
/**
* Set raw
*
* @param string $raw Raw query string typed by user.
*
* @return static
*/
public function setRaw($raw)
{
$this->raw = $raw;
return $this;
}
/**
* Get raw
*
* @return string
*/
public function getRaw()
{
return $this->raw;
}
/**
* Set rawFilters
*
* @param array $rawFilters Raw filters.
*
* @return static
*/
public function setRawFilters(array $rawFilters)
{
$this->rawFilters = $rawFilters;
return $this;
}
/**
* Get rawFilters
*
* @return array
*/
public function getRawFilters()
{
return $this->rawFilters;
}
/**
* Set rawAdvancedFilters
*
* @param array $rawAdvancedFilters Raw filters.
*
* @return static
*/
public function setRawAdvancedFilters(array $rawAdvancedFilters)
{
$this->rawAdvancedFilters = $rawAdvancedFilters;
return $this;
}
/**
* Get rawAdvancedFilters
*
* @return array
*/
public function getRawAdvancedFilters()
{
return $this->rawAdvancedFilters;
}
/**
* Set fields
*
* @param array $fields Array of field involved in search.
*
* @return static
*/
public function setFields(array $fields = [])
{
$this->fields = $fields;
return $this;
}
/**
* Get fields
*
* @return array
*/
public function getFields()
{
return $this->fields;
}
/**
* Set normalized
*
* @param string $normalized Normalized query string.
*
* @return static
*/
public function setNormalized($normalized)
{
$this->normalized = $normalized;
return $this;
}
/**
* Get normalized
*
* @return string
*/
public function getNormalized()
{
return $this->normalized;
}
/**
* Set hash
*
* @param string $hash Query hash.
*
* @return static
*/
public function setHash($hash)
{
$this->hash = $hash;
return $this;
}
/**
* Get hash
*
* @return string
*/
public function getHash()
{
return $this->hash;
}
/**
* Set date
*
* @param \DateTime $date When query was requested.
*
* @return static
*/
public function setDate(\DateTime $date)
{
$this->date = $date;
return $this;
}
/**
* Get date
*
* @return \DateTime
*/
public function getDate()
{
return $this->date;
}
/**
* Add page
*
* @param Page $page A Page entity instance.
*
* @return static
*/
public function addPage(Page $page)
{
$this->pages[] = $page;
$page->setQuery($this);
return $this;
}
/**
* Remove page
*
* @param Page $page A Page entity instance.
*
* @return static
*/
public function removePage(Page $page)
{
$this->pages->removeElement($page);
$page->setQuery(null);
return $this;
}
/**
* Get pages
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getPages()
{
return $this->pages;
}
/**
* Set filters
*
* @param array $filters Array of filters.
*
* @return static
*/
public function setFilters(array $filters)
{
$this->filters = $filters;
return $this;
}
/**
* Get filters
*
* @return array
*/
public function getFilters()
{
return $this->filters;
}
/**
* Create query entity instance from search request instance.
*
* @param SearchRequestInterface $searchRequest A SearchRequestInterface
* instance.
*
* @return static
*/
public static function fromSearchRequest(SearchRequestInterface $searchRequest)
{
$instance = new static();
return $instance
->setFilters($searchRequest->getFilters())
->setFields($searchRequest->getFields())
->setNormalized($searchRequest->getNormalizedQuery())
->setRaw($searchRequest->getQuery())
->setHash($searchRequest->getHash());
}
/**
* Create proper Page entity instance for binding document and current
* collection.
*
* @param integer $number Page number.
*
* @return Page
*/
public function createPage($number)
{
return Page::create()
->setQuery($this)
->setNumber($number);
}
/**
* @return integer
*/
public function getCollectionId()
{
return $this->id;
}
/**
* @return CollectionTypeEnum
*/
public function getCollectionType()
{
return CollectionTypeEnum::query();
}
}
@@ -0,0 +1,64 @@
<?php
namespace CacheBundle\Entity\Query;
use Doctrine\ORM\Mapping as ORM;
/**
* SimpleQuery
*
* @ORM\Entity(
* repositoryClass="CacheBundle\Repository\SimpleQueryRepository"
* )
*/
class SimpleQuery extends AbstractQuery
{
/**
* @var \DateTime
*
* @ORM\Column(type="datetime")
*/
protected $expirationDate;
/**
* Set expirationDate
*
* @param \DateTime|string $expirationDate May be a \DateTime instance or
* string for computing relative to
* query 'date' property.
*
* @return SimpleQuery
*/
public function setExpirationDate($expirationDate)
{
if (is_string($expirationDate)) {
$date = clone $this->date;
$expirationDate = $date->modify($expirationDate);
}
$this->expirationDate = $expirationDate;
return $this;
}
/**
* Get expirationDate
*
* @return \DateTime
*/
public function getExpirationDate()
{
return $this->expirationDate;
}
/**
* Check that this simple query is still fresh.
*
* @return boolean
*/
public function isFresh()
{
return $this->expirationDate >= date_create();
}
}
@@ -0,0 +1,197 @@
<?php
namespace CacheBundle\Entity\Query;
use CacheBundle\Entity\Feed\QueryFeed;
use Common\Enum\StoredQueryStatusEnum;
use Doctrine\ORM\Mapping as ORM;
/**
* StoredQuery
*
* @ORM\Entity(
* repositoryClass="CacheBundle\Repository\StoredQueryRepository"
* )
*/
class StoredQuery extends AbstractQuery
{
/**
* @var boolean
*
* @ORM\Column(type="boolean")
*/
protected $limitExceed = false;
/**
* @var \DateTime
*
* @ORM\Column(type="datetime")
*/
protected $lastUpdateAt;
/**
* @var string
*
* @ORM\Column
*/
protected $status = StoredQueryStatusEnum::INITIALIZE;
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\OneToMany(
* targetEntity="CacheBundle\Entity\Feed\QueryFeed",
* mappedBy="query"
* )
*/
protected $feeds;
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->lastUpdateAt = new \DateTime();
}
/**
* Set limitExceed
*
* @param boolean $limitExceed Flag, if true current stored query exceed
* limit.
*
* @return StoredQuery
*/
public function setLimitExceed($limitExceed)
{
$this->limitExceed = $limitExceed;
return $this;
}
/**
* Get limitExceed
*
* @return boolean
*/
public function isLimitExceed()
{
return $this->limitExceed;
}
/**
* Set lastUpdateAt
*
* @param \DateTime $lastUpdateAt Date of last updated of this query.
*
* @return StoredQuery
*/
public function setLastUpdateAt(\DateTime $lastUpdateAt)
{
$this->lastUpdateAt = $lastUpdateAt;
return $this;
}
/**
* Get lastUpdateAt
*
* @return \DateTime
*/
public function getLastUpdateAt()
{
return $this->lastUpdateAt;
}
/**
* Set status
*
* @param string $status Stored query status.
*
* @return StoredQuery
*/
public function setStatus($status)
{
$this->status = $status;
return $this;
}
/**
* Get status
*
* @return string
*/
public function getStatus()
{
return $this->status;
}
/**
* Checks that this stored query is in specified status.
*
* @param string|string[] $status Stored query status.
*
* @return boolean
*/
public function isInStatus($status)
{
if (is_string($status)) {
$status = [ $status ];
}
return \nspl\a\any($status, \nspl\f\partial('\nspl\op\idnt', $this->status));
}
/**
* Get limitExceed
*
* @return boolean
*/
public function getLimitExceed()
{
return $this->limitExceed;
}
/**
* Add feed
*
* @param QueryFeed $feed A QueryFeed instance.
*
* @return StoredQuery
*/
public function addFeed(QueryFeed $feed)
{
$this->feeds[] = $feed;
$feed->setQuery($this);
return $this;
}
/**
* Remove feed
*
* @param QueryFeed $feed A QueryFeed instance.
*
* @return StoredQuery
*/
public function removeFeed(QueryFeed $feed)
{
$this->feeds->removeElement($feed);
$feed->setQuery(null);
return $this;
}
/**
* Get queries
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getFeeds()
{
return $this->feeds;
}
}
+373
View File
@@ -0,0 +1,373 @@
<?php
namespace CacheBundle\Entity;
use ApiBundle\Entity\NormalizableEntityInterface;
use ApiBundle\Serializer\Metadata\Metadata;
use ApiBundle\Serializer\Metadata\PropertyMetadata;
use AppBundle\Entity\BaseEntityTrait;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use UserBundle\Entity\User;
/**
* Source
*
* @ORM\Table(name="source_list")
* @ORM\Entity(
* repositoryClass="CacheBundle\Repository\SourceListRepository"
* )
* @UniqueEntity({"name", "user"})
* @ORM\HasLifecycleCallbacks
*/
class SourceList implements NormalizableEntityInterface
{
use BaseEntityTrait;
/**
* @var string
*
* @Assert\NotBlank()
* @ORM\Column(type="string")
*/
protected $name;
/**
* @var boolean
*
* @ORM\Column(type="boolean")
*/
protected $isGlobal = false;
/**
* @var \DateTime
*
* @ORM\Column(type="datetime")
*/
protected $createdAt;
/**
* @var \DateTime
*
* @ORM\Column(type="datetime", nullable=true)
*/
protected $updatedAt;
/**
* @var User
*
* @ORM\ManyToOne(targetEntity="UserBundle\Entity\User")
*/
protected $updatedBy;
/**
* @var User
*
* @ORM\ManyToOne(targetEntity="UserBundle\Entity\User", inversedBy="sourcesLists")
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $user;
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\OneToMany(
* targetEntity="CacheBundle\Entity\SourceToSourceList",
* mappedBy="list",
* cascade={ "persist", "remove" }
* )
*/
protected $sources;
/**
* @var integer
*
* @ORM\Column(type="integer")
*/
protected $sourceNumber = 0;
/**
* Constructor
*/
public function __construct()
{
$this->sources = new ArrayCollection();
$this->createdAt = new \DateTime();
}
/**
* @return SourceList
*/
public function cloneList()
{
$clone = clone $this;
$clone
->setSourceNumber(0)
->setUpdatedAt(null)
->setUpdatedBy(null);
$clone->sources = new ArrayCollection();
return $clone;
}
/**
* Return metadata for current entity.
*
* @return \ApiBundle\Serializer\Metadata\Metadata
*/
public function getMetadata()
{
return new Metadata(static::class, [
PropertyMetadata::createInteger('id', [ 'id' ]),
PropertyMetadata::createInteger('sourceNumber', [ 'source_list' ]),
PropertyMetadata::createBoolean('shared', [ 'source_list' ])
->setField('isGlobal'),
PropertyMetadata::createString('name', [ 'source_list' ]),
PropertyMetadata::createEntity('user', User::class, [ 'source_list' ]),
PropertyMetadata::createDate('createdAt', [ 'source_list' ]),
PropertyMetadata::createDate('updatedAt', [ 'source_list' ]),
PropertyMetadata::createEntity('updatedBy', User::class, [ 'source_list' ]),
]);
}
/**
* Return default normalization groups.
*
* @return array
*/
public function defaultGroups()
{
return [ 'source_list', 'id' ];
}
/**
* @ORM\PreUpdate()
*
* @return void
*/
public function onUpdate()
{
$this->setUpdatedAt(new \DateTime());
}
/**
* Set title
*
* @param string $name Source list name.
*
* @return SourceList
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get title
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set createdAt
*
* @param \DateTime $createdAt When this list is created.
*
* @return SourceList
*/
public function setCreatedAt(\DateTime $createdAt = null)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Get createdAt
*
* @return \DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Set updatedAt
*
* @param \DateTime $updatedAt When this list is updated.
*
* @return SourceList
*/
public function setUpdatedAt(\DateTime $updatedAt = null)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* Get updatedAt
*
* @return \DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
/**
* Set updatedBy
*
* @param User $user A User entity instance.
*
* @return SourceList
*/
public function setUpdatedBy(User $user = null)
{
$this->updatedBy = $user;
return $this;
}
/**
* Get updatedBy
*
* @return User
*/
public function getUpdatedBy()
{
return $this->updatedBy;
}
/**
* Set user
*
* @param User $user A owner User entity instance.
*
* @return SourceList
*/
public function setUser(User $user = null)
{
$this->user = $user;
return $this;
}
/**
* Get user
*
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* Set isGlobal
*
* @param boolean $isGlobal Is global source list or not.
*
* @return SourceList
*/
public function setIsGlobal($isGlobal)
{
$this->isGlobal = $isGlobal;
return $this;
}
/**
* Get isGlobal
*
* @return boolean
*/
public function getIsGlobal()
{
return $this->isGlobal;
}
/**
* Check whether specified user owner.
*
* @param User $user A User entity instance.
*
* @return boolean
*/
public function isOwnedBy(User $user)
{
return $user->getId() === $this->user->getId();
}
/**
* Set sourceNumber
*
* @param integer $sourceNumber Sources count.
*
* @return SourceList
*/
public function setSourceNumber($sourceNumber)
{
$this->sourceNumber = $sourceNumber;
return $this;
}
/**
* Get sourceNumber
*
* @return integer
*/
public function getSourceNumber()
{
return $this->sourceNumber;
}
/**
* Add source
*
* @param SourceToSourceList $source A SourceToSourceList entity instance.
*
* @return SourceList
*/
public function addSource(SourceToSourceList $source)
{
$this->sources[] = $source;
return $this;
}
/**
* Remove source
*
* @param SourceToSourceList $source A SourceToSourceList entity instance.
*
* @return SourceList
*/
public function removeSource(SourceToSourceList $source)
{
$this->sources->removeElement($source);
return $this;
}
/**
* Get sources
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getSources()
{
return $this->sources;
}
}
@@ -0,0 +1,105 @@
<?php
namespace CacheBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* SourceToSourceList
* Many to many relation between source list in database and sources in index.
*
* @ORM\Table(name="cross_sources_source_lists")
* @ORM\Entity
*/
class SourceToSourceList
{
/**
* Source id from cache index.
*
* @var string|resource
*
* @ORM\Id
* @ORM\Column(type="binary")
*/
private $source;
/**
* @var SourceList
*
* @ORM\Id
* @ORM\ManyToOne(targetEntity="CacheBundle\Entity\SourceList", inversedBy="sources")
*/
private $list;
/**
* @param string $source Source id from cache index.
* @param SourceList $list A SourceList entity instance.
*
* @return SourceToSourceList
*/
public static function create($source, SourceList $list)
{
$instance = new self();
return $instance
->setSource($source)
->setList($list);
}
/**
* Set source
*
* @param string $source Source id from cache index.
*
* @return SourceToSourceList
*/
public function setSource($source)
{
$this->source = $source;
return $this;
}
/**
* Get source
*
* @return string
*/
public function getSource()
{
if (! is_string($this->source)) {
//
// Because of Doctrine.
//
$this->source = stream_get_contents($this->source);
}
return $this->source;
}
/**
* Set list
*
* @param SourceList $list A SourceList entity instance.
*
* @return SourceToSourceList
*/
public function setList(SourceList $list)
{
$this->list = $list;
return $this;
}
/**
* Get list
*
* @return SourceList
*/
public function getList()
{
return $this->list;
}
}
@@ -0,0 +1,124 @@
<?php
namespace CacheBundle\Feed\Fetcher;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Entity\Feed\ClipFeed;
use CacheBundle\Feed\Response\FeedResponse;
use CacheBundle\Feed\Response\FeedResponseInterface;
use Common\Enum\CollectionTypeEnum;
use Common\Enum\FieldNameEnum;
use Doctrine\ORM\EntityManagerInterface;
use IndexBundle\Index\Internal\InternalIndexInterface;
use IndexBundle\SearchRequest\SearchRequestBuilderInterface;
/**
* Class ClipFeedFetcher
*
* Fetch document and meta information for query feed.
*
* @package CacheBundle\Feed\Fetcher
*/
class ClipFeedFetcher implements FeedFetcherInterface
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var InternalIndexInterface
*/
private $index;
/**
* QueryFeedFetcher constructor.
*
* @param EntityManagerInterface $em A EntityManagerInterface instance.
* @param InternalIndexInterface $index A InternalIndexInterface instance.
*/
public function __construct(
EntityManagerInterface $em,
InternalIndexInterface $index
) {
$this->em = $em;
$this->index = $index;
}
/**
* Fetch information for specified feed
*
* @param AbstractFeed $feed A AbstractFeed entity
* instance.
* @param SearchRequestBuilderInterface $builder A SearchRequestBuilderInterface
* instance.
*
* @return FeedResponseInterface
*/
public function fetch(AbstractFeed $feed, SearchRequestBuilderInterface $builder)
{
if (! $feed instanceof ClipFeed) {
throw new \InvalidArgumentException(
'Expect '. ClipFeed::class . ' but got '. get_class($feed)
);
}
$factory = $this->index->getFilterFactory();
$request = $this->index->createRequestBuilder()
->setFilters($builder->getFilters())
->addFilter($factory->eq(FieldNameEnum::COLLECTION_ID, $feed->getId()))
->addFilter($factory->eq(FieldNameEnum::COLLECTION_TYPE, CollectionTypeEnum::FEED))
->build();
return new FeedResponse(
$request->execute(),
$request->getAvailableAdvancedFilters(), // AFSourceEnum::FEED
[
'type' => 'clip_feed',
'status' => 'synced',
'search' => [
'advancedFilters' => count($feed->getRawFilters()) > 0 ? $feed->getRawFilters() : (object) [],
],
]
);
}
/**
* Create search builder for specified feed.
*
* @param AbstractFeed $feed A AbstractFeed entity instance.
*
* @return SearchRequestBuilderInterface|null
*/
public function createRequestBuilder(AbstractFeed $feed)
{
if (! $feed instanceof ClipFeed) {
throw new \InvalidArgumentException(
'Expect '. ClipFeed::class . ' but got '. get_class($feed)
);
}
$factory = $this->index->getFilterFactory();
$filters = $feed->getFilters();
$filters[] = $factory->eq(FieldNameEnum::COLLECTION_ID, $feed->getId());
$filters[] = $factory->eq(FieldNameEnum::COLLECTION_TYPE, CollectionTypeEnum::FEED);
return $this->index->createRequestBuilder()
->setFilters($filters)
->setFields([
FieldNameEnum::TITLE,
FieldNameEnum::MAIN,
]);
}
/**
* Return supported feed fqcn.
*
* @return string
*/
public static function support()
{
return ClipFeed::class;
}
}
@@ -0,0 +1,26 @@
<?php
namespace CacheBundle\Feed\Fetcher\Factory;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Feed\Fetcher\FeedFetcherInterface;
/**
* Interface FeedFetcherFactoryInterface
*
* Return feed fetcher factory.
*
* @package CacheBundle\Feed\Fetcher\Factory
*/
interface FeedFetcherFactoryInterface
{
/**
* Get feed fetcher for specified feed.
*
* @param string|AbstractFeed $feedClass Feed fqcn or instance.
*
* @return FeedFetcherInterface
*/
public function get($feedClass);
}
@@ -0,0 +1,68 @@
<?php
namespace CacheBundle\Feed\Fetcher\Factory;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Feed\Fetcher\FeedFetcherInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class LazyFeedFetcherFactory
*
* Return feed fetcher factory.
*
* @package CacheBundle\Feed\Fetcher\Factory
*/
class LazyFeedFetcherFactory implements FeedFetcherFactoryInterface
{
/**
* @var ContainerInterface
*/
private $container;
/**
* @var array
*/
private $map;
/**
* LazyFeedFetcherFactory constructor.
*
* @param ContainerInterface $container A ContainerInterface instance.
* @param array $map Map between feed fqcn and proper
* fetcher service id.
*/
public function __construct(ContainerInterface $container, array $map)
{
$this->container = $container;
$this->map = $map;
}
/**
* Get feed fetcher for specified feed.
*
* @param string|AbstractFeed $feedClass Feed fqcn.
*
* @return FeedFetcherInterface
*/
public function get($feedClass)
{
if (is_object($feedClass)) {
$feedClass = get_class($feedClass);
}
if (! is_string($feedClass)) {
throw new \InvalidArgumentException(
'Invalid parameter feedClass. Should be string or instance of AbstractFeed.'
);
}
$fetcher = $this->container->get($this->map[$feedClass]);
if (! $fetcher instanceof FeedFetcherInterface) {
throw new \RuntimeException('Got invalid fetcher.');
}
return $fetcher;
}
}
@@ -0,0 +1,46 @@
<?php
namespace CacheBundle\Feed\Fetcher;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Feed\Response\FeedResponseInterface;
use IndexBundle\SearchRequest\SearchRequestBuilderInterface;
/**
* Interface FeedFetcherInterface
*
* Fetch document and meta information for feeds.
*
* @package CacheBundle\Feed\Fetcher
*/
interface FeedFetcherInterface
{
/**
* Fetch information for specified feed
*
* @param AbstractFeed $feed A AbstractFeed entity
* instance.
* @param SearchRequestBuilderInterface $builder A SearchRequestBuilderInterface
* instance.
*
* @return FeedResponseInterface
*/
public function fetch(AbstractFeed $feed, SearchRequestBuilderInterface $builder);
/**
* Create search builder for specified feed.
*
* @param AbstractFeed $feed A AbstractFeed entity instance.
*
* @return SearchRequestBuilderInterface|null
*/
public function createRequestBuilder(AbstractFeed $feed);
/**
* Return supported feed fqcn.
*
* @return string
*/
public static function support();
}
@@ -0,0 +1,180 @@
<?php
namespace CacheBundle\Feed\Fetcher;
use AppBundle\AdvancedFilters\AdvancedFiltersConfig;
use AppBundle\Manager\Source\SourceManagerInterface;
use AppBundle\Manager\StoredQuery\StoredQueryManagerInterface;
use AppBundle\Response\SearchResponse;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Entity\Feed\QueryFeed;
use CacheBundle\Feed\Response\FeedResponse;
use CacheBundle\Feed\Response\FeedResponseInterface;
use CacheBundle\Repository\StoredQueryRepository;
use Common\Enum\AFSourceEnum;
use Common\Enum\CollectionTypeEnum;
use Common\Enum\FieldNameEnum;
use Common\Enum\StoredQueryStatusEnum;
use Doctrine\ORM\EntityManagerInterface;
use IndexBundle\SearchRequest\SearchRequestBuilderInterface;
/**
* Class QueryFeedFetcher
*
* Fetch document and meta information for query feed.
*
* @package CacheBundle\Feed\Fetcher
*/
class QueryFeedFetcher implements FeedFetcherInterface
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var StoredQueryManagerInterface
*/
private $manager;
/**
* @var SourceManagerInterface
*/
private $sourceManager;
/**
* QueryFeedFetcher constructor.
*
* @param EntityManagerInterface $em A EntityManagerInterface
* instance.
* @param StoredQueryManagerInterface $manager A StoredQueryManagerInterface
* instance.
* @param SourceManagerInterface $sourceManager A SourceManagerInterface
* instance.
*/
public function __construct(
EntityManagerInterface $em,
StoredQueryManagerInterface $manager,
SourceManagerInterface $sourceManager
) {
$this->em = $em;
$this->manager = $manager;
$this->sourceManager = $sourceManager;
}
/**
* Fetch information for specified feed
*
* @param AbstractFeed $feed A AbstractFeed entity
* instance.
* @param SearchRequestBuilderInterface $builder A SearchRequestBuilderInterface
* instance.
*
* @return FeedResponseInterface
*/
public function fetch(AbstractFeed $feed, SearchRequestBuilderInterface $builder)
{
if (! $feed instanceof QueryFeed) {
throw new \InvalidArgumentException(
'Expect '. QueryFeed::class . ' but got '. get_class($feed)
);
}
//
// Get proper stored query for fetched feed.
//
/** @var StoredQueryRepository $repository */
$repository = $this->em->getRepository('CacheBundle:Query\StoredQuery');
$query = $repository->getByFeed($feed->getId());
//
// Collect information.
//
$queryStatus = $query->isInStatus([
StoredQueryStatusEnum::INITIALIZE,
StoredQueryStatusEnum::DELETED,
]);
$response = new SearchResponse();
$sources = $this->sourceManager->getSourcesForQuery($query, [ 'id', 'title', 'type' ]);
$sourceLists = $this->sourceManager->getSourceListsForQuery($query, [ 'id', 'name' ]);
$meta = [
'type' => 'query_feed',
'status' => $queryStatus ? 'not_synced' : 'synced',
'search' => [
'query' => $query->getRaw(),
'filters' => $query->getRawFilters(),
'advancedFilters' => count($query->getRawAdvancedFilters()) > 0 ? $query->getRawAdvancedFilters() : (object) [],
],
'sources' => $sources,
'sourceLists' => $sourceLists,
];
$advancedFilters = AdvancedFiltersConfig::getDefault(AFSourceEnum::FEED);
if (! $query->isInStatus([
StoredQueryStatusEnum::INITIALIZE,
StoredQueryStatusEnum::DELETED,
])) {
$factory = $builder->getIndex()->getFilterFactory();
$builder
->addFilter($factory->andX([
$factory->eq(FieldNameEnum::COLLECTION_ID, $query->getId()),
$factory->eq(FieldNameEnum::COLLECTION_TYPE, CollectionTypeEnum::QUERY),
]))
->addFilter($factory->not($factory->eq(FieldNameEnum::DELETE_FROM, $feed->getId())));
$response = $this->manager->get($feed->getUser(), $query, $builder);
$advancedFilters = $this->manager->getAdvancedFilters($query, $builder);
}
return new FeedResponse($response, $advancedFilters, $meta);
}
/**
* Create search builder for specified feed.
*
* @param AbstractFeed $feed A AbstractFeed entity instance.
*
* @return SearchRequestBuilderInterface|null
*/
public function createRequestBuilder(AbstractFeed $feed)
{
if (! $feed instanceof QueryFeed) {
throw new \InvalidArgumentException(
'Expect '. QueryFeed::class . ' but got '. get_class($feed)
);
}
//
// Get proper stored query for fetched feed.
//
/** @var StoredQueryRepository $repository */
$repository = $this->em->getRepository('CacheBundle:Query\StoredQuery');
$query = $repository->getByFeed($feed->getId());
if (! $query->isInStatus([
StoredQueryStatusEnum::INITIALIZE,
StoredQueryStatusEnum::DELETED,
])) {
return $this->manager->createRequestBuilder(
$feed->getUser(),
$query
);
}
return null;
}
/**
* Return supported feed fqcn.
*
* @return string
*/
public static function support()
{
return QueryFeed::class;
}
}
@@ -0,0 +1,107 @@
<?php
namespace CacheBundle\Feed\Formatter;
use AppBundle\Manager\Feed\FeedManagerInterface;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Feed\Formatter\Strategy\FeedFormatterStrategyInterface;
use Common\Enum\FieldNameEnum;
use Common\Enum\FormatNameEnum;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class BasicFeedFormatter
*
* @package CacheBundle\Feed\Formatter
*/
class BasicFeedFormatter implements FeedFormatterInterface
{
/**
* @var FeedManagerInterface
*/
private $feedManager;
/**
* @var ContainerInterface
*/
private $container;
/**
* BasicFeedFormatter constructor.
*
* @param FeedManagerInterface $feedManager A FeedManagerInterface instance.
* @param ContainerInterface $container A ContainerInterface instance.
*/
public function __construct(
FeedManagerInterface $feedManager,
ContainerInterface $container
) {
$this->feedManager = $feedManager;
$this->container = $container;
}
/**
* Format feed documents.
*
* @param AbstractFeed $feed A formatted feed entity instance.
* @param FormatterOptions $options Used format options.
*
* @return FormattedData
*/
public function formatFeed(AbstractFeed $feed, FormatterOptions $options)
{
$strategy = $this->createStrategy($options->getFormat());
$filterFactory = $this->feedManager->getIndex()->getFilterFactory();
$sourceFields = $strategy->requiredFields($options);
$sourceFields[] = FieldNameEnum::SEQUENCE;
$sourceFields[] = FieldNameEnum::COLLECTION_ID;
$sourceFields[] = FieldNameEnum::COLLECTION_TYPE;
$documents = $this->feedManager->getIndex()->createRequestBuilder()
->setFilters($filterFactory->andX([
$filterFactory->eq(FieldNameEnum::COLLECTION_ID, $feed->getCollectionId()),
$filterFactory->eq(FieldNameEnum::COLLECTION_TYPE, $feed->getCollectionType()),
]))
->setSources($sourceFields)
->setLimit($options->getNumberOfDocuments())
->setSorts([ FieldNameEnum::PUBLISHED => 'desc' ])
->build()
->execute()
->getDocuments();
return new FormattedData(
$strategy->serialize($feed, $documents, $options),
$strategy->getMime()
);
}
/**
* Create strategy for specified format.
*
* @param FormatNameEnum $format Used format name.
*
* @return FeedFormatterStrategyInterface
*/
private function createStrategy(FormatNameEnum $format)
{
$name = 'cache.feed_formatter_strategy.'. strtolower($format->getValue());
if (! $this->container->has($name)) {
throw new \InvalidArgumentException('Unknown format '. $format->getValue());
}
$strategy = $this->container->get($name);
if (! $strategy instanceof FeedFormatterStrategyInterface) {
throw new \InvalidArgumentException(sprintf(
'Feed formatter strategy should implements %s interface.',
FeedFormatterStrategyInterface::class
));
}
return $strategy;
}
}
@@ -0,0 +1,24 @@
<?php
namespace CacheBundle\Feed\Formatter;
use CacheBundle\Entity\Feed\AbstractFeed;
/**
* Interface FeedFormatterInterface
*
* @package CacheBundle\Feed\Formatter
*/
interface FeedFormatterInterface
{
/**
* Format feed documents.
*
* @param AbstractFeed $feed A formatted feed entity instance.
* @param FormatterOptions $options Used format options.
*
* @return FormattedData
*/
public function formatFeed(AbstractFeed $feed, FormatterOptions $options);
}
@@ -0,0 +1,50 @@
<?php
namespace CacheBundle\Feed\Formatter;
/**
* Class FormattedData
*
* @package CacheBundle\Feed\Formatter
*/
class FormattedData
{
/**
* @var mixed
*/
private $data;
/**
* @var string
*/
private $mime;
/**
* FormattedData constructor.
*
* @param mixed $data Formatted data.
* @param string $mime Data mime type.
*/
public function __construct($data, $mime)
{
$this->data = $data;
$this->mime = $mime;
}
/**
* @return mixed
*/
public function getData()
{
return $this->data;
}
/**
* @return string
*/
public function getMime()
{
return $this->mime;
}
}
@@ -0,0 +1,126 @@
<?php
namespace CacheBundle\Feed\Formatter;
use Common\Enum\FormatNameEnum;
use UserBundle\Enum\ThemeOptionExtractEnum;
/**
* Class FormatterOptions
*
* @package CacheBundle\Feed\Formatter
*/
class FormatterOptions
{
/**
* @var FormatNameEnum
*/
private $format;
/**
* @var integer
*/
private $numberOfDocuments;
/**
* @var ThemeOptionExtractEnum
*/
private $extract;
/**
* @var boolean
*/
private $showImages;
/**
* @var boolean
*/
private $asPlain;
/**
* @var boolean
*/
private $highlight;
/**
* FormatterOptions constructor.
*
* @param FormatNameEnum $format A FormatNameEnum instance.
* @param integer $numberOfDocuments A required number of
* documents.
* @param ThemeOptionExtractEnum|null $extract A ThemeOptionExtractEnum
* instance. No extract
* if null.
* @param boolean $showImages Should fetch image
* url or not.
* @param boolean $asPlain Show content as plain
* text.
* @param boolean $highlight Should highlight
* matched keywords or
* not.
*/
public function __construct(
FormatNameEnum $format,
$numberOfDocuments = 1,
ThemeOptionExtractEnum $extract = null,
$showImages = false,
$asPlain = false,
$highlight = false
) {
$this->format = $format;
$this->numberOfDocuments = $numberOfDocuments;
$this->extract = $extract ?: ThemeOptionExtractEnum::no();
$this->showImages = $showImages;
$this->asPlain = $asPlain;
$this->highlight = $highlight;
}
/**
* @return FormatNameEnum
*/
public function getFormat()
{
return $this->format;
}
/**
* @return integer
*/
public function getNumberOfDocuments()
{
return $this->numberOfDocuments;
}
/**
* @return ThemeOptionExtractEnum
*/
public function getExtract()
{
return $this->extract;
}
/**
* @return boolean
*/
public function isShowImages()
{
return $this->showImages;
}
/**
* @return boolean
*/
public function isAsPlain()
{
return $this->asPlain;
}
/**
* @return boolean
*/
public function isHighlight()
{
return $this->highlight;
}
}
@@ -0,0 +1,79 @@
<?php
namespace CacheBundle\Feed\Formatter\Strategy;
use CacheBundle\Document\Extractor\DocumentContentExtractorInterface;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Entity\Feed\QueryFeed;
use CacheBundle\Feed\Formatter\FormatterOptions;
use Common\Enum\FieldNameEnum;
use UserBundle\Enum\ThemeOptionExtractEnum;
/**
* Class AbstractFeedFormatStrategy
*
* @package CacheBundle\Feed\Formatter\Strategy
*/
abstract class AbstractFeedFormatStrategy implements FeedFormatterStrategyInterface
{
/**
* @var DocumentContentExtractorInterface
*/
private $extractor;
/**
* AbstractFeedFormatStrategy constructor.
*
* @param DocumentContentExtractorInterface $extractor A DocumentContentExtractorInterface
* instance.
*/
public function __construct(DocumentContentExtractorInterface $extractor)
{
$this->extractor = $extractor;
}
/**
* Return list of required document fields.
*
* @param FormatterOptions $options Formatter options.
*
* @return string[]
*/
public function requiredFields(FormatterOptions $options)
{
$fields = [];
if (! $options->getExtract()->is(ThemeOptionExtractEnum::NO)) {
$fields[] = FieldNameEnum::MAIN;
}
return $fields;
}
/**
* @param string $content Document content.
* @param FormatterOptions $options FormatterOptions.
* @param AbstractFeed $feed A serialized feed entity instance.
*
* @return string
*/
protected function extract($content, FormatterOptions $options, AbstractFeed $feed)
{
$extract = $options->getExtract();
//
// We should get normalized search query only if it requested.
//
$query = '';
if (! $extract->is(ThemeOptionExtractEnum::no())) {
if ($feed instanceof QueryFeed) {
$query = $feed->getQuery()->getNormalized();
}
}
$result = $this->extractor->extract($content, $query, $extract);
return $result->getText() . ($result->getLength() > 0 ? '...' : '');
}
}
@@ -0,0 +1,139 @@
<?php
namespace CacheBundle\Feed\Formatter\Strategy;
use CacheBundle\Document\Extractor\DocumentContentExtractorInterface;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Feed\Formatter\FormatterOptions;
use Common\Enum\FieldNameEnum;
use IndexBundle\Model\ArticleDocumentInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Class AtomFeedFormatterStrategy
*
* @package CacheBundle\Feed\Formatter\Strategy
*
* @link https://validator.w3.org/feed/docs/atom.html
*/
class AtomFeedFormatterStrategy extends AbstractFeedFormatStrategy
{
/**
* @var UrlGeneratorInterface
*/
private $generator;
/**
* AtomFeedFormatterStrategy constructor.
*
* @param DocumentContentExtractorInterface $extractor A
* DocumentContentExtractorInterface
* instance.
* @param UrlGeneratorInterface $generator A UrlGeneratorInterface
* instance.
*/
public function __construct(
DocumentContentExtractorInterface $extractor,
UrlGeneratorInterface $generator
) {
parent::__construct($extractor);
$this->generator = $generator;
}
/**
* Return list of required document fields.
*
* @param FormatterOptions $options Formatter options.
*
* @return string[]
*/
public function requiredFields(FormatterOptions $options)
{
$fields = parent::requiredFields($options);
$fields[] = FieldNameEnum::TITLE;
$fields[] = FieldNameEnum::PERMALINK;
$fields[] = FieldNameEnum::SOURCE_TITLE;
$fields[] = FieldNameEnum::SOURCE_LINK;
$fields[] = FieldNameEnum::PUBLISHED;
$fields[] = FieldNameEnum::SECTION;
if ($options->isShowImages()) {
$fields[] = FieldNameEnum::IMAGE_SRC;
}
return $fields;
}
/**
* Serialize feed.
*
* @param AbstractFeed $feed A serialized feed entity
* instance.
* @param ArticleDocumentInterface[] $documents Array of fetched documents
* which should by serialized.
* @param FormatterOptions $options Formatter options.
*
* @return mixed
*/
public function serialize(
AbstractFeed $feed,
array $documents,
FormatterOptions $options
) {
$node = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"></feed>');
// Attach feed info.
$node->addChild('id', $this->generator->generate('app_index_index', [], UrlGeneratorInterface::ABSOLUTE_URL));
$node->addChild('title', $feed->getName());
$node->addChild('updated', date_create()->format('c'));
$link = $node->addChild('link');
$link->addAttribute('rel', 'self');
$link->addAttribute('href', $this->generator->generate('app_index_exportfeed', [
'format' => $options->getFormat()->getValue(),
'id' => $feed->getId(),
]));
$textType = $options->isAsPlain() ? 'text' : 'html';
foreach ($documents as $document) {
$data = $document->getNormalizedData();
$item = $node->addChild('entry');
$item
->addChild('title', $data['title'])
->addAttribute('type', $textType);
$link = $item->addChild('link');
$link->addAttribute('rel', 'alternate');
$link->addAttribute('href', $data['permalink']);
if ($options->isShowImages()) {
$item->addChild('image', $data['image']);
}
$author = $item->addChild('author');
$author->addChild('name', $data['source']['title']);
$author->addChild('uri', $data['source']['link']);
$item->addChild('pubDate', $data['published']->format('d M Y H:i:s e'));
$item
->addChild('summary', $this->extract($data['content'], $options, $feed))
->addAttribute('type', $textType);
}
return $node->asXML();
}
/**
* Get format mime type.
*
* @return string
*/
public function getMime()
{
return 'text/xml';
}
}
@@ -0,0 +1,49 @@
<?php
namespace CacheBundle\Feed\Formatter\Strategy;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Feed\Formatter\FormatterOptions;
use IndexBundle\Model\ArticleDocumentInterface;
/**
* Interface FeedFormatterStrategyInterface
*
* @package CacheBundle\Feed\Formatter\Strategy
*/
interface FeedFormatterStrategyInterface
{
/**
* Return list of required document fields.
*
* @param FormatterOptions $options Formatter options.
*
* @return string[]
*/
public function requiredFields(FormatterOptions $options);
/**
* Serialize feed.
*
* @param AbstractFeed $feed A serialized feed entity
* instance.
* @param ArticleDocumentInterface[] $documents Array of fetched documents
* which should by serialized.
* @param FormatterOptions $options Formatter options.
*
* @return mixed
*/
public function serialize(
AbstractFeed $feed,
array $documents,
FormatterOptions $options
);
/**
* Get format mime type.
*
* @return string
*/
public function getMime();
}
@@ -0,0 +1,105 @@
<?php
namespace CacheBundle\Feed\Formatter\Strategy;
use CacheBundle\Document\Extractor\DocumentContentExtractorInterface;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Feed\Formatter\FormatterOptions;
use Common\Enum\FieldNameEnum;
use IndexBundle\Model\ArticleDocumentInterface;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
/**
* Class HtmlFeedFormatterStrategy
*
* @package CacheBundle\Feed\Formatter\Strategy
*/
class HtmlFeedFormatterStrategy extends AbstractFeedFormatStrategy
{
/**
* @var EngineInterface
*/
private $templating;
/**
* HtmlFeedFormatterStrategy constructor.
*
* @param DocumentContentExtractorInterface $extractor A DocumentContentExtractorInterface
* instance.
* @param EngineInterface $templating A templating EngineInterface
* instance.
*/
public function __construct(
DocumentContentExtractorInterface $extractor,
EngineInterface $templating
) {
parent::__construct($extractor);
$this->templating = $templating;
}
/**
* Return list of required document fields.
*
* @param FormatterOptions $options Formatter options.
*
* @return string[]
*/
public function requiredFields(FormatterOptions $options)
{
$fields = parent::requiredFields($options);
$fields[] = FieldNameEnum::PERMALINK;
$fields[] = FieldNameEnum::SOURCE_TITLE;
$fields[] = FieldNameEnum::SOURCE_LINK;
$fields[] = FieldNameEnum::PUBLISHED;
$fields[] = FieldNameEnum::AUTHOR_NAME;
$fields[] = FieldNameEnum::TITLE;
if ($options->isShowImages()) {
$fields[] = FieldNameEnum::IMAGE_SRC;
}
return $fields;
}
/**
* Serialize feed.
*
* @param AbstractFeed $feed A serialized feed entity
* instance.
* @param ArticleDocumentInterface[] $documents Array of fetched documents
* which should by serialized.
* @param FormatterOptions $options Formatter options.
*
* @return mixed
*/
public function serialize(
AbstractFeed $feed,
array $documents,
FormatterOptions $options
) {
$data = \nspl\a\map(function (ArticleDocumentInterface $document) use ($options, $feed) {
$data = $document->getNormalizedData();
$data['content'] = $this->extract($data['content'], $options, $feed);
return $data;
}, $documents);
return $this->templating->render('CacheBundle::feed.html.twig', [
'feed' => $feed,
'data' => $data,
]);
}
/**
* Get format mime type.
*
* @return string
*/
public function getMime()
{
return 'text/html';
}
}
@@ -0,0 +1,120 @@
<?php
namespace CacheBundle\Feed\Formatter\Strategy;
use CacheBundle\Document\Extractor\DocumentContentExtractorInterface;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Feed\Formatter\FormatterOptions;
use Common\Enum\FieldNameEnum;
use IndexBundle\Model\ArticleDocumentInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Class RssFeedFormatterStrategy
*
* @package CacheBundle\Feed\Formatter\Strategy
*
* @link https://validator.w3.org/feed/docs/rss2.html
*/
class RssFeedFormatterStrategy extends AbstractFeedFormatStrategy
{
/**
* @var UrlGeneratorInterface
*/
private $generator;
/**
* AtomFeedFormatterStrategy constructor.
*
* @param DocumentContentExtractorInterface $extractor A
* DocumentContentExtractorInterface
* instance.
* @param UrlGeneratorInterface $generator A UrlGeneratorInterface
* instance.
*/
public function __construct(
DocumentContentExtractorInterface $extractor,
UrlGeneratorInterface $generator
) {
parent::__construct($extractor);
$this->generator = $generator;
}
/**
* Return list of required document fields.
*
* @param FormatterOptions $options Formatter options.
*
* @return string[]
*/
public function requiredFields(FormatterOptions $options)
{
$fields = parent::requiredFields($options);
$fields[] = FieldNameEnum::TITLE;
$fields[] = FieldNameEnum::PERMALINK;
$fields[] = FieldNameEnum::SOURCE_TITLE;
$fields[] = FieldNameEnum::SOURCE_LINK;
$fields[] = FieldNameEnum::PUBLISHED;
if ($options->isShowImages()) {
$fields[] = FieldNameEnum::IMAGE_SRC;
}
return $fields;
}
/**
* Serialize feed.
*
* @param AbstractFeed $feed A serialized feed entity
* instance.
* @param ArticleDocumentInterface[] $documents Array of fetched documents
* which should by serialized.
* @param FormatterOptions $options Formatter options.
*
* @return mixed
*/
public function serialize(
AbstractFeed $feed,
array $documents,
FormatterOptions $options
) {
$node = new \SimpleXMLElement('<rss version="2.0"></rss>');
// Attach channel info.
$channel = $node->addChild('channel');
$channel->addChild('title', $feed->getName());
$channel->addChild('link', $this->generator->generate('app_index_index', [], UrlGeneratorInterface::ABSOLUTE_URL));
foreach ($documents as $document) {
$data = $document->getNormalizedData();
$item = $channel->addChild('item');
$item
->addChild('title', $data['title'])
->addAttribute('url', $data['permalink']);
if ($options->isShowImages()) {
$item->addChild('image', $data['image']);
}
$item->addChild('description', $data['content']);
$item
->addChild('source', $data['source']['title'])
->addAttribute('url', $data['source']['link']);
$item->addChild('pubDate', $data['published']->format('d M Y H:i:s e'));
}
return $node->asXML();
}
/**
* Get format mime type.
*
* @return string
*/
public function getMime()
{
return 'text/xml';
}
}
@@ -0,0 +1,113 @@
<?php
namespace CacheBundle\Feed\Formatter\Strategy;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Feed\Formatter\FormatterOptions;
use Common\Enum\FieldNameEnum;
use IndexBundle\Model\ArticleDocumentInterface;
/**
* Class TsvFeedFormatterStrategy
*
* @package CacheBundle\Feed\Formatter\Strategy
*
* @link http://www.iana.org/assignments/media-types/text/tab-separated-values
*/
class TsvFeedFormatterStrategy extends AbstractFeedFormatStrategy
{
/**
* TSV column titles.
*
* @var string[]
*/
private static $columns = [
'Link',
'Headline Text',
'Source Name',
'Source URL',
'Harvest Time',
'Extractor Author',
'Content',
];
/**
* Return list of required document fields.
*
* @param FormatterOptions $options Formatter options.
*
* @return string[]
*/
public function requiredFields(FormatterOptions $options)
{
$fields = parent::requiredFields($options);
$fields[] = FieldNameEnum::PERMALINK;
$fields[] = FieldNameEnum::SOURCE_TITLE;
$fields[] = FieldNameEnum::SOURCE_LINK;
$fields[] = FieldNameEnum::PUBLISHED;
$fields[] = FieldNameEnum::AUTHOR_NAME;
return $fields;
}
/**
* Serialize feed.
*
* @param AbstractFeed $feed A serialized feed entity
* instance.
* @param ArticleDocumentInterface[]|array $documents Array of fetched documents
* which should by serialized.
* @param FormatterOptions $options Formatter options.
*
* @return mixed
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function serialize(
AbstractFeed $feed,
array $documents,
FormatterOptions $options
) {
$body = implode("\t", self::$columns) . PHP_EOL;
$processor = \nspl\f\compose(
\nspl\f\partial('implode', "\t"),
function (ArticleDocumentInterface $document) use ($options, $feed) {
$data = $document->getNormalizedData();
$date = $data['published'];
if (! $date instanceof \DateTimeInterface) {
$date = date_create();
}
return [
$data['permalink'],
$data['title'],
$data['source']['title'],
$data['source']['link'],
$date->format('Y-m-d H:i:s'),
$data['author']['name'],
$this->extract($data['content'], $options, $feed),
];
}
);
$lines = \nspl\a\map($processor, $documents);
return $body . implode(PHP_EOL, $lines);
}
/**
* Get format mime type.
*
* @return string
*/
public function getMime()
{
//
// TSV has own mime type: 'text/tab-separated-values' but response with
// this mime got strange encoding so we use 'text/plain' instead.
//
return 'text/plain';
}
}
@@ -0,0 +1,88 @@
<?php
namespace CacheBundle\Feed\Response;
use AppBundle\Response\SearchResponseInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Class FeedResponse
* @package CacheBundle\Feed\Response
*/
class FeedResponse implements FeedResponseInterface
{
/**
* @var SearchResponseInterface
*/
private $response;
/**
* @var array
*/
private $advancedFilters;
/**
* @var array
*/
private $meta;
/**
* FeedResponse constructor.
*
* @param SearchResponseInterface $response A SearchResponseInterface
* instance.
* @param array $advancedFilters Advanced filters.
* @param array $meta Response meta information.
*/
public function __construct(
SearchResponseInterface $response,
array $advancedFilters = [],
array $meta = []
) {
$this->response = $response;
$this->advancedFilters = $advancedFilters;
$this->meta = $meta;
}
/**
* Get response.
*
* @return SearchResponseInterface
*/
public function getResponse()
{
return $this->response;
}
/**
* Get available advanced filters values.
*
* @return array
*/
public function getAdvancedFilters()
{
return $this->advancedFilters;
}
/**
* Get response meta information.
*
* @param Request $request A Request instance.
*
* @return array
*/
public function getMeta(Request $request)
{
$currentAdvancedFilters = $request->request->get('advancedFilters', []);
//
// Add currently selected advanced filters if they are provided.
//
if (count($currentAdvancedFilters)) {
$this->meta['search']['advancedFilters'] = $currentAdvancedFilters;
}
return $this->meta;
}
}
@@ -0,0 +1,36 @@
<?php
namespace CacheBundle\Feed\Response;
use Symfony\Component\HttpFoundation\Request;
/**
* Interface FeedResponseInterface
* @package CacheBundle\Feed\Response
*/
interface FeedResponseInterface
{
/**
* Get response.
*
* @return \AppBundle\Response\SearchResponseInterface
*/
public function getResponse();
/**
* Get response meta information.
*
* @param Request $request A Request instance.
*
* @return array
*/
public function getMeta(Request $request);
/**
* Get available advanced filters values.
*
* @return array
*/
public function getAdvancedFilters();
}
+174
View File
@@ -0,0 +1,174 @@
<?php
namespace CacheBundle\Form;
use AppBundle\Form\SearchRequest\AbstractSearchRequestType;
use AppBundle\Form\Type\FiltersType;
use CacheBundle\DTO\AnalyticDTO;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Form\Type\CurrentUserOwnedEntityType;
use Doctrine\Common\Collections\Collection;
use IndexBundle\Index\IndexInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* Class AnalyticType
*
* Transform http request into AnalyticDTO object.
*
* @package CacheBundle\Form
*/
class AnalyticType extends AbstractType implements DataMapperInterface
{
/**
* @var IndexInterface
*/
private $index;
/**
* @var TokenStorageInterface
*/
private $tokenStorage;
/**
* @var array
*/
private $rawFilters;
/**
* AnalyticType constructor.
*
* @param IndexInterface $index A IndexInterface instance.
* @param TokenStorageInterface $tokenStorage A TokenStorageInterface instance.
*/
public function __construct(
IndexInterface $index,
TokenStorageInterface $tokenStorage
) {
$this->index = $index;
$this->tokenStorage = $tokenStorage;
}
/**
* 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('feeds', CurrentUserOwnedEntityType::class, [
'class' => AbstractFeed::class,
'multiple' => true,
'description' => 'Array of current user feeds ids.',
'constraints' => new NotBlank(),
])
->add('filters', FiltersType::class, [
'filter_factory' => $this->index->getFilterFactory(),
'description' => 'Search filters.',
'empty_data' => [],
'filters' => AbstractSearchRequestType::$filters,
'required' => false,
])
->setDataMapper($this)
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
$this->rawFilters = [];
if (isset($data['filters'])) {
$this->rawFilters = $data['filters'];
}
});
}
/**
* 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' => AnalyticDTO::class,
'empty_data' => null,
]);
}
/**
* Returns the prefix of the template block name for this type.
*
* The block prefix defaults to the underscored short class name with
* the "Type" suffix removed (e.g. "UserProfileType" => "user_profile").
*
* @return string The prefix of the template block name.
*/
public function getBlockPrefix()
{
return '';
}
/**
* Maps properties of some data to a list of forms.
*
* @param AnalyticDTO|null $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 not necessary method.
}
/**
* 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 AnalyticDTO|null $data Structured data.
*
* @return void
*/
public function mapFormsToData($forms, &$data)
{
$forms = iterator_to_array($forms);
$feeds = $forms['feeds']->getData();
if ($feeds instanceof Collection) {
$feeds = $feeds->toArray();
}
$data = new AnalyticDTO(
$feeds,
\app\op\invokeIf($this->tokenStorage->getToken(), 'getUser'),
$forms['filters']->getData(),
$this->rawFilters
);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace CacheBundle\Form;
use CacheBundle\Entity\Category;
use CacheBundle\Form\Type\CurrentUserOwnedEntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class CategoryType
* @package CacheBundle\Form
*/
class CategoryType 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('name')
->add('parent', CurrentUserOwnedEntityType::class, [
'class' => Category::class,
]);
}
/**
* 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', Category::class);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace CacheBundle\Form;
use CacheBundle\Entity\Comment;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class CommentType
* @package CacheBundle\Form
*/
class CommentType 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('title', null, [ 'empty_data' => '' ])
->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', Comment::class);
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
namespace CacheBundle\Form;
use CacheBundle\Entity\Category;
use CacheBundle\Entity\Document;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Entity\Feed\ClipFeed;
use CacheBundle\Entity\Feed\QueryFeed;
use CacheBundle\Form\Type\CurrentUserOwnedEntityType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* Class FeedInfoType
*
* @package CacheBundle\Form
*/
class FeedInfoType extends AbstractType 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)
{
$availableSubtypes = [
ClipFeed::getSubType(),
QueryFeed::getSubType(),
];
$builder
->add('subType', ChoiceType::class, [
'choices' => $availableSubtypes,
'invalid_message' => sprintf(
'Unknown feed sub type. Available: %s',
implode(', ', $availableSubtypes)
),
])
->add('excludedDocuments', EntityType::class, [
'class' => Document::class,
'multiple' => true,
])
->add('name', null, [
'constraints' => new NotBlank(),
])
->add('category', CurrentUserOwnedEntityType::class, [
'class' => Category::class,
])
->setDataMapper($this);
}
/**
* 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' => AbstractFeed::class,
'empty_data' => null,
'validation_groups' => [ 'Feed_Create' ],
]);
}
/**
* Maps properties of some data to a list of forms.
*
* @param AbstractFeed|null $data Structured data.
* @param FormInterface[]|\RecursiveIteratorIterator $forms A list of
* {@link FormInterface}
* instances.
*
* @return void
*/
public function mapDataToForms($data, $forms)
{
$forms = iterator_to_array($forms);
if ($data instanceof AbstractFeed) {
$forms['name']->setData($data->getName());
$forms['category']->setData($data->getCategory());
}
}
/**
* 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 AbstractFeed|null $data Structured data.
*
* @return void
*/
public function mapFormsToData($forms, &$data)
{
$forms = iterator_to_array($forms);
//
// Create new proper feed instance if it's not provided to us.
//
if (! $data instanceof AbstractFeed) {
try {
$data = AbstractFeed::createBySubType($forms['subType']->getData());
} catch (\Exception $exception) {
// This should be handled by constraints.
}
}
$data
->setName($forms['name']->getData())
->setCategory($forms['category']->getData());
$excludedDocuments = $forms['excludedDocuments']->getData();
foreach ($excludedDocuments as $document) {
$data->addExcludedDocument($document);
}
}
}
@@ -0,0 +1,118 @@
<?php
namespace CacheBundle\Form\Sources;
use AppBundle\Form\Transformer\OnlyReverseTransformerTrait;
use CacheBundle\Form\Sources\Type\SortType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class SourceListSearchType
* @package CacheBundle\Form\Sources
*/
class SourceListSearchType extends AbstractType implements DataTransformerInterface
{
use OnlyReverseTransformerTrait;
public static $fields = [
'name' => 'name',
'sources' => 'sourceNumber',
'createdBy' => 'user',
'lastUpdated' => 'updatedAt',
'lastUpdatedBy' => 'updatedBy',
];
/**
* 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('page', null, [
'description' => 'Requested page number, should start from 1. Default value 1.',
'empty_data' => 1,
])
->add('limit', null, [
'description' => 'Max sources per page. Default 20.',
'empty_data' => 20,
])
->add('sort', SortType::class, [
'fields' => self::$fields,
'default_field' => 'name',
'default_direction' => 'asc',
])
->add('onlyShared', CheckboxType::class, [
'description' => 'Show only shared source lists.',
'required' => false,
])
->addModelTransformer($this);
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('key', 'searchSourceList');
}
/**
* Transforms a value from the transformed representation to its original
* representation.
*
* This method is called when {@link Form::submit()} is called to transform
* the requests tainted data into an acceptable format for your data
* processing/model layer.
*
* This method must be able to deal with empty values. Usually this will
* be an empty string, but depending on your implementation other empty
* values are possible as well (such as NULL). The reasoning behind
* this is that value transformers must be chainable. If the
* reverseTransform() method of the first value transformer outputs an
* empty string, the second value transformer must be able to process that
* value.
*
* By convention, reverseTransform() should return NULL if an empty string
* is passed.
*
* @param mixed $data The value in the transformed representation.
*
* @return mixed The value in the original representation
*
* @throws TransformationFailedException When the transformation fails.
*/
public function reverseTransform($data)
{
if (count($data['sort']) === 0) {
$data['sort'] = [ 'name' => 'asc' ];
}
if (! isset($data['onlyShared'])) {
$data['onlyShared'] = false;
}
return $data;
}
}
@@ -0,0 +1,47 @@
<?php
namespace CacheBundle\Form\Sources;
use CacheBundle\Entity\SourceList;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class SourceListType
* @package CacheBundle\Form\Sources
*/
class SourceListType 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('name');
}
/**
* 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', SourceList::class);
}
}
@@ -0,0 +1,191 @@
<?php
namespace CacheBundle\Form\Sources;
use AppBundle\AdvancedFilters\AdvancedFiltersConfig;
use AppBundle\Form\AbstractConnectionAwareType;
use AppBundle\Form\Transformer\OnlyReverseTransformer;
use AppBundle\Form\Type\AdvancedFiltersType;
use AppBundle\Form\Type\Filter as QueryFilter;
use AppBundle\Form\Type\FiltersType;
use CacheBundle\Form\Sources\Type\SortType;
use Common\Enum\AFSourceEnum;
use Common\Enum\FieldNameEnum;
use IndexBundle\SearchRequest\SearchRequestBuilderInterface;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length;
/**
* Class SourceSearchType
* @package CacheBundle\Form\Sources
*/
class SourceSearchType extends AbstractConnectionAwareType implements DataMapperInterface
{
public static $fields = [
'name' => FieldNameEnum::SOURCE_TITLE,
'mediaType' => FieldNameEnum::SOURCE_PUBLISHER_TYPE,
'country' => FieldNameEnum::COUNTRY,
];
private static $filters = [
'publisher' => [
'type' => QueryFilter\PublisherFilterType::class,
'description' => 'Filter by publisher type.',
],
'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.',
],
];
/**
* 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)
{
/**
* Custom data mapping
*
* @param FormEvent $event A FormEvent instance.
*
* @return void
*/
$postSubmit = function (FormEvent $event) {
/** @var SearchRequestBuilderInterface $builder */
$builder = $event->getData();
$builder->setSorts($event->getForm()->get('sort')->getData());
$event->setData($builder);
};
$builder
->add('query', null, [
'description' => 'Search query, maybe empty. Search by source title and url.',
'empty_data' => '',
'constraints' => new Length([
'max' => 40,
'maxMessage' => 'Search query is too long. Should be 40 characters long or less.',
]),
])
->add('page', null, [
'description' => 'Requested page number, should start from 1. Default value 1.',
'empty_data' => 1,
])
->add('limit', null, [
'description' => 'Max sources per page. Default 20.',
'empty_data' => 20,
])
->add('filters', FiltersType::class, [
'filter_factory' => $this->index->getFilterFactory(),
'description' => 'Search filters.',
'empty_data' => [],
'filters' => self::$filters,
])
->add('sort', SortType::class, [
'fields' => self::$fields,
'default_field' => 'name',
'default_direction' => 'asc',
'mapped' => false,
])
->add('advancedFilters', AdvancedFiltersType::class, [
'description' => 'Advanced filters.',
'config' => AdvancedFiltersConfig::getConfig(AFSourceEnum::SOURCE),
'empty_data' => [],
'connection' => $this->index,
'required' => false,
])
->addEventListener(FormEvents::POST_SUBMIT, $postSubmit)
->setEmptyData($this->index->createRequestBuilder())
->setDataMapper($this);
$builder
->get('sort')
->addModelTransformer(new OnlyReverseTransformer(function (array $data) {
if (count($data) === 0) {
// Default sort order.
$data = [ FieldNameEnum::SOURCE_TITLE => 'asc' ];
}
return $data;
}));
}
/**
* 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', 'searchSource');
}
/**
* 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
->setQuery($forms['query']->getData())
->setPage($forms['page']->getData())
->setLimit($forms['limit']->getData())
->setFilters(array_merge(
$forms['filters']->getData(),
$forms['advancedFilters']->getData()
));
}
}
@@ -0,0 +1,78 @@
<?php
namespace CacheBundle\Form\Sources\Type;
use AppBundle\Form\Transformer\OnlyReverseTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class SortType
* @package CacheBundle\Form\Sources\Type
*/
class SortType 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)
{
$fields = $options['fields'];
$builder
->add('field', ChoiceType::class, [
'description' => 'Field name on which we should sort.',
'choices' => array_keys($options['fields']),
'empty_data' => $options['default_field'],
])
->add('direction', ChoiceType::class, [
'description' => 'Sorting direction.',
'choices' => [ 'asc', 'desc' ],
'empty_data' => $options['default_direction'],
]);
$transformer = new OnlyReverseTransformer(function (array $sortObject) use ($fields) {
if (! is_array($sortObject)) {
throw new TransformationFailedException('Expect array got '. gettype($sortObject));
}
if (! isset($sortObject['field'], $sortObject['direction'])) {
return [];
}
return [ $fields[$sortObject['field']] => $sortObject['direction'] ];
});
$builder->addModelTransformer($transformer);
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setRequired('fields')
->setDefined('default_field')
->setDefined('default_direction')
->setAllowedTypes('fields', 'array');
}
}
@@ -0,0 +1,83 @@
<?php
namespace CacheBundle\Form\Type;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use UserBundle\Entity\User;
/**
* Class CurrentUserCategoryType
*
* Extend default EntityType with proper query builder which is returns categories
* which is owned by current user.
*
* @package CacheBundle\Form\Type
*/
class CurrentUserOwnedEntityType extends EntityType
{
/**
* @var TokenStorageInterface
*/
private $storage;
/**
* CurrentUserOwnedEntityType constructor.
*
* @param ManagerRegistry $managerRegistry A ManagerRegistry instance.
* @param TokenStorageInterface $storage A TokenStorageInterface
* instance.
*/
public function __construct(
ManagerRegistry $managerRegistry,
TokenStorageInterface $storage
) {
parent::__construct($managerRegistry);
$this->storage = $storage;
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*
* @return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setRequired('user_property')
->setAllowedTypes('user_property', 'string')
->setDefaults([
'user_property' => 'user',
'query_builder' => function (Options $options) {
$userProperty = $options['user_property'];
return function (EntityRepository $repository) use ($userProperty) {
// Get current user.
$user = \app\op\invokeIf($this->storage->getToken(), 'getUser');
if ($user instanceof User) {
$user = $user->getId();
}
$qb = $repository->createQueryBuilder('Entity');
if ($user instanceof User) {
$qb
->where("Entity.{$userProperty} = :user")
->setParameter('user', $user);
}
return $qb;
};
},
]);
parent::configureOptions($resolver);
}
}
@@ -0,0 +1,39 @@
<?php
namespace CacheBundle\Repository;
use CacheBundle\Entity\Analytic\Analytic;
use Doctrine\ORM\EntityRepository;
/**
* Class AnalyticRepository
* @package CacheBundle\Repository
*/
class AnalyticRepository extends EntityRepository
{
/**
* Get array of specified user analytics.
*
* @param integer $user A User entity id.
*
* @return Analytic[]
*/
public function getList($user)
{
$expr = $this->_em->getExpressionBuilder();
return $this->createQueryBuilder('Analytic')
->select(
'partial Analytic.{id,createdAt,updatedAt}',
'partial context.{hash,filters,rawFilters}'
)
->leftJoin('Analytic.context', 'context')
->where($expr->eq('Analytic.owner', ':user'))
->setParameter('user', $user)
->addOrderBy('Analytic.id', 'desc')
->getQuery()
->getResult();
}
}
@@ -0,0 +1,221 @@
<?php
namespace CacheBundle\Repository;
use CacheBundle\Entity\Category;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\ORM\EntityRepository;
/**
* Class CategoryRepository
* @package CacheBundle\Repository
*/
class CategoryRepository extends EntityRepository
{
/**
* Get single entity from repository.
*
* @param integer $id A Entity instance id.
* @param string $method A CRUD method name.
*
* @return Category|null
*/
public function getOne($id, $method)
{
$expr = $this->_em->getExpressionBuilder();
$condition = $expr->andX($expr->eq('Category.id', ':id'));
$parameters = [ 'id' => $id ];
if ($method !== 'get') {
$condition->add($expr->eq('Category.internal', false));
}
return $this->createQueryBuilder('Category')
->addSelect('partial Query.{id, raw, status}')
->where($condition)
->setParameters($parameters)
->getQuery()
->getOneOrNullResult();
}
/**
* Compute feed count in specified category and all childes categories.
*
* @param integer $id A Category entity id.
*
* @return integer
*/
public function computeFeedCounts($id)
{
return (int) $this->_em->getConnection()->fetchColumn("
SELECT COUNT(feeds.id)
FROM feeds
WHERE
category_id in (
SELECT id
FROM (
SELECT *
FROM categories
ORDER BY parent_id, id
) categories_sorted,
(SELECT @pv := '{$id}') initialisation
WHERE
(
FIND_IN_SET(parent_id, @pv) > 0
OR id = {$id}
)
AND @pv := CONCAT(@pv, ',', id)
)
");
}
/**
* Export all feeds inside this category.
*
* @param integer $category A Category entity id.
* @param boolean $export Export all feeds if true and unexport otherwise.
*
* @return void
*/
public function exportFeedsIn($category, $export = true)
{
$this->_em->getConnection()->transactional(function (Connection $conn) use ($category, $export) {
$conn->exec(sprintf("
UPDATE feeds
SET exported = %d
WHERE
category_id in (
SELECT id
FROM (
SELECT *
FROM categories
ORDER BY parent_id, id
) categories_sorted,
(SELECT @pv := '%s') initialisation
WHERE
(
FIND_IN_SET(parent_id, @pv) > 0
OR id = %s
)
AND @pv := CONCAT(@pv, ',', id)
)
", $export, $category, $category));
$conn->exec(sprintf("
UPDATE categories
SET exported = %d
WHERE
id in (
SELECT id
FROM (
SELECT *
FROM categories
ORDER BY parent_id, id
) categories_sorted,
(SELECT @pv := '%s') initialisation
WHERE
(
FIND_IN_SET(parent_id, @pv) > 0
OR id = %s
)
AND @pv := CONCAT(@pv, ',', id)
)
", $export, $category, $category));
});
}
/**
* Get active category.
*
* @param integer $id A Category entity id.
* @param integer $user Filter categories by specified owner if set.
* @param string|array $type Filter by category types.
*
* @return Category|null
*/
public function get($id, $user = null, $type = null)
{
$expr = $this->_em->getExpressionBuilder();
$condition = $expr->andX($expr->eq('Category.id', ':id'));
$parameters = [ 'id' => $id ];
if ($type) {
$type = (array) $type;
$condition->add($expr->in('Category.type', (array) $type));
}
if ($user !== null) {
$condition->add($expr->eq('Category.user', ':user'));
$parameters['user'] = $user;
}
return $this->createQueryBuilder('Category')
->where($condition)
->setParameters($parameters)
->getQuery()
->getOneOrNullResult();
}
/**
* Get array of specified user categories.
*
* @param integer $user A User entity id.
*
* @return Category[]
*/
public function getList($user)
{
$expr = $this->_em->getExpressionBuilder();
return $this->createQueryBuilder('Category')
->select(
'partial Category.{id, name, type}',
'partial Child.{id, name, type}',
'Feed'
)
->leftJoin('Category.parent', 'Parent')
->leftJoin('Category.childes', 'Child')
->leftJoin('Category.feeds', 'Feed')
->where($expr->andX(
$expr->eq('Category.user', ':user'),
$expr->isNull('Category.parent')
))
->setParameter('user', $user)
->getQuery()
->getResult();
}
/**
* Check that specified 'child' category is child of 'parent'.
*
* @param integer $child A Category entity id which may by child.
* @param integer $parent A Category entity id which must by parent of
* specified child.
*
* @return boolean
*/
public function isChildOf($child, $parent)
{
//
// TODO: May be exists more efficient way to do it.
//
$position = (int) $this->_em
->getConnection()
->fetchColumn("
SELECT
FIND_IN_SET({$child}, lvl) AS result
FROM (
SELECT
GROUP_CONCAT(lvl SEPARATOR ',') AS lvl
FROM (
SELECT @parent := (SELECT GROUP_CONCAT(id SEPARATOR ',')
FROM categories
WHERE FIND_IN_SET(parent_id, @parent)
) AS lvl FROM categories JOIN (SELECT @parent := {$parent}) tmp ) a ) b;
");
return $position !== 0;
}
}
@@ -0,0 +1,68 @@
<?php
namespace CacheBundle\Repository;
use CacheBundle\Entity\Category;
use CacheBundle\Entity\Feed\ClipFeed;
use Doctrine\ORM\EntityRepository;
use UserBundle\Entity\User;
/**
* ClipFeedRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class ClipFeedRepository extends EntityRepository
{
/**
* Get read later clip feed.
*
* @param integer $user Owner of read later feed.
*
* @return \CacheBundle\Entity\Feed\AbstractFeed|null
*/
public function getReadLater($user)
{
return $this->createQueryBuilder('Feed')
->where('Feed.user = :user AND Feed.name = :name')
->setParameters([
'user' => $user,
'name' => ClipFeed::READ_LATER,
])
->getQuery()
->getOneOrNullResult();
}
/**
* Create read later feed.
*
* @param integer $user Owner of read later feed.
*
* @return \CacheBundle\Entity\Feed\AbstractFeed
*/
public function createReadLater($user)
{
$mainCategory = $this->_em->createQueryBuilder()
->select('partial Category.{id}')
->from(Category::class, 'Category')
->where('Category.user = :user AND Category.type = :type')
->setParameters([
'user' => $user,
'type' => Category::TYPE_MY_CONTENT,
])
->getQuery()
->getOneOrNullResult();
$feed = ClipFeed::create()
->setName(ClipFeed::READ_LATER)
->setUser($this->_em->getReference(User::class, $user))
->setCategory($mainCategory);
$this->_em->persist($feed);
$this->_em->flush($feed);
return $feed;
}
}
@@ -0,0 +1,79 @@
<?php
namespace CacheBundle\Repository;
use Doctrine\ORM\EntityRepository;
/**
* Class CommentRepository
* @package CacheBundle\Repository
*/
class CommentRepository extends EntityRepository
{
/**
* Get list of comments for documents.
*
* @param integer $document A Document entity id.
* @param array $fields Array of comment fields. Fetch only specified
* fields if set.
* @param integer $count Number of comments.
*
* @return \Doctrine\ORM\QueryBuilder
*/
public function getListForDocument($document, array $fields = [], $count = null)
{
$qb = $this->createQueryBuilder('Comment')
->where('Comment.document = :document')
->addOrderBy('Comment.createdAt', 'desc')
->setParameter('document', $document);
if (count($fields) > 0) {
$authorFields = [];
if (isset($fields['author'])) {
$authorFields = $fields['author'];
unset($fields['author']);
}
$qb->select('partial Comment.{id, '. implode(',', $fields) .'}');
if (count($authorFields) > 0) {
$qb
->join('Comment.author', 'Author')
->addSelect('partial Author.{id, '. implode(',', $authorFields) .'}');
}
} else {
$qb
->join('Comment.author', 'Author')
->addSelect('Author');
}
if ($count !== null) {
$qb->setMaxResults($count);
}
return $qb;
}
/**
* @param integer $documentId A Document entity id.
* @param integer $poolSize Max new comments for specified document.
*
* @return void
*/
public function updateCommentMarks($documentId, $poolSize)
{
$this->_em->getConnection()->executeUpdate(sprintf('
UPDATE comments
SET new = 0
WHERE id IN (
SELECT id
FROM (
SELECT id
FROM comments
WHERE document_id = :document AND new = 1
ORDER BY created_at DESC LIMIT %d, %d
) AS
u)
', $poolSize, 1000), [ 'document' => $documentId ]);
}
}
@@ -0,0 +1,39 @@
<?php
namespace CacheBundle\Repository;
use Doctrine\ORM\EntityRepository;
/**
* Class CommonFeedRepository
* @package CacheBundle\Repository
*/
class CommonFeedRepository extends EntityRepository
{
/**
* Get single feed from repository.
*
* @param integer $id A Feed entity instance id.
* @param integer $user Filter feeds by specified owner if set.
*
* @return \CacheBundle\Entity\Feed\AbstractFeed|null
*/
public function getOne($id, $user)
{
$expr = $this->_em->getExpressionBuilder();
$condition = $expr->andX($expr->eq('Feed.id', ':id'));
$parameters = [ 'id' => $id ];
if ($user !== null) {
$condition->add($expr->eq('Feed.user', ':user'));
$parameters['user'] = $user;
}
return $this->createQueryBuilder('Feed')
->where($condition)
->setParameters($parameters)
->getQuery()
->getOneOrNullResult();
}
}
@@ -0,0 +1,253 @@
<?php
namespace CacheBundle\Repository;
use CacheBundle\Entity\Document;
use Common\Enum\CollectionTypeEnum;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\Expr\Join;
/**
* Class DocumentRepository
* @package CacheBundle\Repository
*/
class DocumentRepository extends EntityRepository
{
/**
* Get document for specified query.
*
* @param integer $query A Query entity id.
* @param integer $page Request page number.
*
* @return Document[]
*/
public function getForQuery($query, $page)
{
$expr = $this->_em->getExpressionBuilder();
return $this->createQueryBuilder('Document')
->addSelect('Comment, CommentAuthor')
->join('Document.pages', 'Page')
->leftJoin('Document.comments', 'Comment', Join::WITH, $expr->andX(
$expr->eq('Comment.document', 'Document.id'),
$expr->eq('Comment.new', 1)
))
->leftJoin('Comment.author', 'CommentAuthor')
->where($expr->andX(
$expr->eq('Page.number', ':number'),
$expr->eq('Page.query', ':query')
))
->setParameters([
'number' => $page,
'query' => $query,
])
->addOrderBy('Comment.createdAt', 'desc')
->getQuery()
->getResult();
}
/**
* Get documents by given ids for specified query without related pages.
* Result will be ordered depends on specified ids order.
*
* @param integer $collectionId A document collection entity id.
* @param string $collectionType A document collection type.
* @param array $ids Array of document ids.
* @param string[]|array $fields Array of required document fields. Fetch
* all if empty.
*
* @return Document[]
*/
public function getFromCollectionByIds($collectionId, $collectionType, array $ids, array $fields = [])
{
$expr = $this->_em->getExpressionBuilder();
$condition = $expr->andX($expr->in('Document.id', $ids));
switch ($collectionType) {
case CollectionTypeEnum::FEED:
$condition->add($expr->eq('Page.clipFeed', ':collectionId'));
break;
case CollectionTypeEnum::QUERY:
$condition->add($expr->eq('Page.query', ':collectionId'));
break;
default:
throw new \InvalidArgumentException('Invalid collection type.');
}
$select = 'Document';
if (count($fields) > 0) {
$select = 'partial Document.{'. implode(',', $fields). '}';
}
$results = $this->createQueryBuilder('Document')
->select($select)
->addSelect('Comment, CommentAuthor, Page')
->join('Document.pages', 'Page')
->leftJoin('Document.comments', 'Comment', Join::WITH, $expr->andX(
$expr->eq('Comment.document', 'Document.id'),
$expr->eq('Comment.new', 1)
))
->leftJoin('Comment.author', 'CommentAuthor')
->where($condition)
->setParameter('collectionId', $collectionId)
//
// We should clearly say how we want to order because mysql does not
// guarantee what result will be ordered by primary key.
//
->addOrderBy('Document.id', 'asc')
->addOrderBy('Comment.createdAt', 'desc')
->getQuery()
->getResult();
//
// Now we use specified ids as index for fetched documents. Since we got
// ordered result we may use binary search for fetching proper document
// by id.
//
$orderedResult = [];
foreach ($ids as $id) {
$idx = \app\a\binarySearch($results, $id, \nspl\op\methodCaller('getId'));
if ($idx === false) {
continue;
}
$orderedResult[] = $results[$idx];
}
return $orderedResult;
}
/**
* Get documents by given ids for specified query.
*
* @param array $ids Array of document ids.
*
* @return Document[]
*/
public function getByIds(array $ids)
{
$expr = $this->_em->getExpressionBuilder();
return $this->createQueryBuilder('Document')
->addSelect('Comment, CommentAuthor')
->where($expr->in('Document.id', $ids))
->leftJoin('Document.comments', 'Comment', Join::WITH, $expr->andX(
$expr->eq('Comment.document', 'Document.id'),
$expr->eq('Comment.new', 1)
))
->leftJoin('Comment.author', 'CommentAuthor')
->addOrderBy('Comment.createdAt', 'desc')
->getQuery()
->getResult();
}
/**
* @param array $fields Array of required fields names, `id` return
* always.
* @param array $ids Array of document ids.
*
* @return Document[]
*/
public function getWithFieldsByIds(array $fields, array $ids)
{
return $this->createQueryBuilder('Document')
->select('partial Document.{id, '.implode(',', $fields).'}')
->where('Document.id IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->getResult();
}
/**
* Check whether the documents exists.
*
* @param string[] $checkedIds Array of document ids.
*
* @return string[] Array of not exists ids from passed.
*/
public function checkIds(array $checkedIds)
{
$expr = $this->_em->getExpressionBuilder();
$existsIds = $this->createQueryBuilder('Document')
->select('Document.id')
->where($expr->in('Document.id', array_map(\nspl\op\str, $checkedIds)))
->getQuery()
->getArrayResult();
return array_diff($checkedIds, \nspl\a\flatten($existsIds));
}
/**
* Remove from provided ids whose document id which already exists in specified document collection.
*
* @param string $collectionId A DocumentCollectionInterface entity id.
* @param string $collectionType A DocumentCollectionInterface type.
* @param array $ids Array of document ids.
*
* @return string[] Array of documents which is not exists in specified document collection.
*/
public function sanitizeIds($collectionId, $collectionType, array $ids)
{
$expr = $this->_em->getExpressionBuilder();
$condition = $expr->andX($expr->in('Document.id', array_map(\nspl\op\str, $ids)));
switch ($collectionType) {
case CollectionTypeEnum::FEED:
$condition->add($expr->eq('Page.clipFeed', ':collectionId'));
break;
case CollectionTypeEnum::QUERY:
$condition->add($expr->eq('Page.query', ':collectionId'));
break;
default:
throw new \InvalidArgumentException('Invalid collection type.');
}
$existsIds = $this->createQueryBuilder('Document')
->select('Document.id')
->join('Document.pages', 'Page')
->where($condition)
->setParameter('collectionId', $collectionId)
->getQuery()
->getArrayResult();
return array_diff($ids, \nspl\a\flatten($existsIds));
}
/**
* @param array $queryId
* @return array
*/
public function getByQuery(array $queryId)
{
return $this->createQueryBuilder('Document')
->select('Document.data','Query.id')
->join('Document.pages', 'Page')
->join('Page.query', 'Query')
->where('Query.id IN (:queryId)')
->setParameter('queryId', $queryId)
->getQuery()
->getResult();
}
/**
* @param array $clipFeedId
* @return array
*/
public function getByClip(array $clipFeedId)
{
return $this->createQueryBuilder('Document')
->select('Document.data','IDENTITY(Page.clipFeed) as clipFeedId ')
->join('Document.pages', 'Page')
->where('Page.clipFeed IN (:clipFeedId)')
->setParameter('clipFeedId', $clipFeedId)
->getQuery()
->getResult();
}
}
@@ -0,0 +1,70 @@
<?php
namespace CacheBundle\Repository;
use CacheBundle\Entity\Query\StoredQuery;
use Doctrine\ORM\EntityRepository;
/**
* QueryFeedRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class QueryFeedRepository extends EntityRepository
{
/**
* Get single query feed from repository.
*
* @param integer $id A Feed entity instance id.
* @param integer $user Filter feeds by specified owner if set.
*
* @return \CacheBundle\Entity\Feed\AbstractFeed|null
*/
public function getOne($id, $user)
{
$expr = $this->_em->getExpressionBuilder();
$condition = $expr->andX($expr->eq('Feed.id', ':id'));
$parameters = [ 'id' => $id ];
if ($user !== null) {
$condition->add($expr->eq('Feed.user', ':user'));
$parameters['user'] = $user;
}
return $this->createQueryBuilder('Feed')
->where($condition)
->setParameters($parameters)
->getQuery()
->getOneOrNullResult();
}
/**
* @param integer $user A User entity id.
*
* @return \Doctrine\ORM\QueryBuilder
*/
public function getFeedsForFormQb($user)
{
return $this->createQueryBuilder('ch')
->where('ch.userId = :userId')
->setParameter(':userId', $user);
}
/**
* @param integer $query A stored query instance.
*
* @return StoredQuery[]
*/
public function getWithExcludedDocumentsForQuery($query)
{
return $this->createQueryBuilder('Feed')
->addSelect('partial Document.{id}')
->innerJoin('Feed.excludedDocuments', 'Document')
->where('Feed.query = :query')
->setParameter('query', $query)
->getQuery()
->getResult();
}
}
@@ -0,0 +1,21 @@
<?php
namespace CacheBundle\Repository;
/**
* Interface QueryRepositoryInterface
* @package CacheBundle\Repository
*/
interface QueryRepositoryInterface
{
/**
* Get query entity by internal representation of search query.
*
* @param string $hash A search query hash.
* @param integer $user A User entity id, who made search request.
*
* @return \CacheBundle\Entity\Query\AbstractQuery|null
*/
public function get($hash, $user = null);
}
@@ -0,0 +1,63 @@
<?php
namespace CacheBundle\Repository;
use Doctrine\ORM\EntityRepository;
/**
* Class SimpleQueryRepository
* @package CacheBundle\Repository
*/
class SimpleQueryRepository extends EntityRepository implements
QueryRepositoryInterface
{
/**
* Get query entity by internal representation of search query.
*
* @param string $hash A search query hash.
* @param integer $user A User entity id, who made search request.
*
* @return \CacheBundle\Entity\Query\SimpleQuery|null
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function get($hash, $user = null)
{
$expr = $this->_em->getExpressionBuilder();
return $this->createQueryBuilder('Query')
->select('partial Query.{id, totalCount, expirationDate, raw, rawFilters, rawAdvancedFilters}')
->where($expr->andX(
$expr->eq('Query.hash', ':hash'),
$expr->gt('Query.expirationDate', ':date')
))
->setParameters([
'hash' => $hash,
'date' => date_create(),
])
->getQuery()
->getOneOrNullResult();
}
/**
* Get all old queries.
*
* @return integer[] Old queries ids.
*/
public function getOld()
{
$expr = $this->_em->getExpressionBuilder();
// Get all old queries ids.
$ids = $this->createQueryBuilder('Query')
->select('Query.id')
->where($expr->lte('Query.expirationDate', ':date'))
->setParameter('date', date_create())
->getQuery()
->getArrayResult();
return array_map(function (array $row) {
return $row['id'];
}, $ids);
}
}
@@ -0,0 +1,108 @@
<?php
namespace CacheBundle\Repository;
use CacheBundle\Entity\SourceList;
use Doctrine\ORM\EntityRepository;
/**
* SourceListRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class SourceListRepository extends EntityRepository
{
/**
* Get QueryBuilder for list of sourceLists for the user
*
* @param integer $user A User entity id.
* @param array $order Array of order config where key - is field name
* and value is order direction.
* @param boolean $onlyShared Show only shared source lists if set.
*
* @return \Doctrine\ORM\QueryBuilder
*/
public function getSourcesListsQB($user, array $order = [], $onlyShared = false)
{
$expr = $this->_em->getExpressionBuilder();
$qb = $this->createQueryBuilder('SourceList')
->addSelect('partial User.{id, firstName, lastName}')
->join('SourceList.user', 'User')
->where($expr->eq('SourceList.user', ':user'))
->setParameter(':user', $user);
if ($onlyShared) {
$qb->andWhere($expr->eq('SourceList.isGlobal', 1));
} else {
$qb->orWhere($expr->eq('SourceList.isGlobal', 1));
}
foreach ($order as $field => $direction) {
$qb->addOrderBy("SourceList.{$field}", $direction);
}
return $qb;
}
/**
* Get concrete SourceLists for the user.
*
* @param integer $id A SourceList entity id.
* @param integer $user A User entity id.
*
* @return SourceList|null
*/
public function getSourcesLists($id, $user)
{
$expr = $this->_em->getExpressionBuilder();
return $this->getSourcesListsQB($user)
->andWhere($expr->eq('SourceList.id', ':id'))
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult();
}
/**
* Remove all source list ids which not exists, not global or not owned by
* specified user.
*
* @param array $ids List of requested source list ids.
*
* @param integer $user A User entity id.
*
* @return integer[]
*/
public function sanitizeIds(array $ids, $user)
{
$expr = $this->_em->getExpressionBuilder();
return array_map(function (array $result) {
return $result['id'];
}, $this->getSourcesListsQB($user)
->select('SourceList.id')
->andWhere($expr->in('SourceList', ':ids'))
->setParameter('ids', $ids)
->getQuery()
->getArrayResult());
}
/**
* @param integer $user A User entity id.
*
* @return integer[]
*/
public function getAvailableIdsForUser($user)
{
return \nspl\a\map(
\nspl\op\itemGetter('id'),
$this->getSourcesListsQB($user)
->select('SourceList.id')
->getQuery()
->getArrayResult()
);
}
}
@@ -0,0 +1,80 @@
<?php
namespace CacheBundle\Repository;
use CacheBundle\Entity\Query\StoredQuery;
use Common\Enum\StoredQueryStatusEnum;
use Doctrine\ORM\EntityRepository;
/**
* Class SimpleQueryRepository
* @package CacheBundle\Repository
*/
class StoredQueryRepository extends EntityRepository implements
QueryRepositoryInterface
{
/**
* Get query entity by internal representation of search query.
*
* @param string $hash A search query hash.
* @param integer $user A User entity id, who made search request.
*
* @return \CacheBundle\Entity\Query\StoredQuery|null
*/
public function get($hash, $user = null)
{
$expr = $this->_em->getExpressionBuilder();
$condition = $expr->andX($expr->eq('Query.hash', ':hash'));
$parameters = [ 'hash' => $hash ];
if ($user !== null) {
$condition->add($expr->eq('Query.user', ':user'));
$parameters['user'] = $user;
}
return $this->createQueryBuilder('Query')
->select('partial Query.{id, totalCount}')
->where($condition)
->setParameters($parameters)
->getQuery()
->getOneOrNullResult();
}
/**
* List stored queries ready for updating.
*
* @return \Doctrine\ORM\QueryBuilder
*/
public function getForUpdating()
{
$expr = $this->_em->getExpressionBuilder();
return $this->createQueryBuilder('Query')
->distinct('Query.id')
->join('Query.feeds', 'feeds')
->where($expr->andX(
$expr->neq('Query.status', ':status'),
$expr->eq('Query.limitExceed', 0)
))
->setParameter('status', StoredQueryStatusEnum::INITIALIZE);
}
/**
* Get Query by feed
*
* @param integer $feed A Feed entity id.
*
* @return StoredQuery
*/
public function getByFeed($feed)
{
return $this->createQueryBuilder('Query')
->leftJoin('Query.feeds', 'feeds')
->where('feeds.id = :feedId')
->setParameter(':feedId', $feed)
->getQuery()
->getOneOrNullResult();
}
}
@@ -0,0 +1,13 @@
<?php
namespace CacheBundle\Repository;
/**
* UserStoredQueryRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class UserStoredQueryRepository extends \Doctrine\ORM\EntityRepository
{
}
@@ -0,0 +1,32 @@
services:
#
# Fetcher factories.
#
cache.feed_fetcher_factory.lazy:
class: 'CacheBundle\Feed\Fetcher\Factory\LazyFeedFetcherFactory'
arguments:
- '@service_container'
- # Injected by compiler phase
cache.feed_fetcher_factory: '@cache.feed_fetcher_factory.lazy'
#
# Fetchers.
#
cache.feed_fetcher.query:
class: 'CacheBundle\Feed\Fetcher\QueryFeedFetcher'
arguments:
- '@doctrine.orm.default_entity_manager'
- '@app.stored_query_manager'
- '@app.source_manager'
tags:
- { name: socialhose.feed_fetcher }
cache.feed_fetcher.clip:
class: 'CacheBundle\Feed\Fetcher\ClipFeedFetcher'
arguments:
- '@doctrine.orm.default_entity_manager'
- '@index.articles'
tags:
- { name: socialhose.feed_fetcher }
@@ -0,0 +1,42 @@
services:
#
# Fetchers.
#
cache.feed_formatter.basic:
class: 'CacheBundle\Feed\Formatter\BasicFeedFormatter'
arguments:
- '@app.feed_manager'
- '@service_container'
cache.feed_formatter: '@cache.feed_formatter.basic'
#
# Strategies.
#
cache.feed_formatter_strategy.abstract:
class: 'CacheBundle\Feed\Formatter\Strategy\AbstractFeedFormatStrategy'
arguments:
- '@cache.document_content_extractor'
abstract: true
cache.feed_formatter_strategy.tsv:
class: 'CacheBundle\Feed\Formatter\Strategy\TsvFeedFormatterStrategy'
parent: 'cache.feed_formatter_strategy.abstract'
cache.feed_formatter_strategy.rss:
class: 'CacheBundle\Feed\Formatter\Strategy\RssFeedFormatterStrategy'
parent: 'cache.feed_formatter_strategy.abstract'
arguments:
- '@router'
cache.feed_formatter_strategy.html:
class: 'CacheBundle\Feed\Formatter\Strategy\HtmlFeedFormatterStrategy'
parent: 'cache.feed_formatter_strategy.abstract'
arguments:
- '@templating'
cache.feed_formatter_strategy.atom:
class: 'CacheBundle\Feed\Formatter\Strategy\AtomFeedFormatterStrategy'
parent: 'cache.feed_formatter_strategy.abstract'
arguments:
- '@router'
@@ -0,0 +1,35 @@
services:
cache.form_type.current_user_category:
class: 'CacheBundle\Form\Type\CurrentUserOwnedEntityType'
arguments:
- '@doctrine'
- '@security.token_storage'
tags:
- { name: form.type }
cache.form.source_list_creation:
class: 'CacheBundle\Form\Sources\SourceListType'
tags:
- { name: form.type }
app.form_factory.filter_factory_aware.source:
class: 'AppBundle\Form\Factory\FilterFactoryAwareTypeFactory'
arguments:
- '@index.sources'
- '%search.page_size%'
public: false
cache.form.soure_index.search:
class: 'CacheBundle\Form\Sources\SourceSearchType'
factory: [ '@app.form_factory.filter_factory_aware.source', 'create' ]
arguments: [ 'CacheBundle\Form\Sources\SourceSearchType' ]
tags:
- { name: form.type }
app.form.analytic:
class: 'CacheBundle\Form\AnalyticType'
arguments:
- '@index.articles'
- '@security.token_storage'
tags:
- { name: form.type }
@@ -0,0 +1,39 @@
services:
#
# Category inspector.
#
cache.inspector.category:
class: 'CacheBundle\Security\Inspector\CategoryInspector'
tags:
- { name: socialhose.inspector }
#
# Comment inspector.
#
cache.inspector.comment:
class: 'CacheBundle\Security\Inspector\CommentInspector'
tags:
- { name: socialhose.inspector }
#
# Query feed inspector.
#
cache.inspector.query_feed:
class: 'CacheBundle\Security\Inspector\FeedInspector'
tags:
- { name: socialhose.inspector }
#
# Source list inspector
#
cache.inspector.source_list:
class: 'CacheBundle\Security\Inspector\SourceListInspector'
tags:
- { name: socialhose.inspector }
#
# Analytic inspector
#
cache.inspector.analytic:
class: 'CacheBundle\Security\Inspector\AnalyticInspector'
tags:
- { name: socialhose.inspector }
@@ -0,0 +1,42 @@
imports:
- { resource: inspectors.yml }
- { resource: forms.yml }
- { resource: feed_fetchers.yml }
- { resource: feed_formatters.yml }
services:
cache.repository.category:
class: 'CacheBundle\Repository\CategoryRepository'
factory: [ '@doctrine.orm.default_entity_manager', 'getRepository' ]
arguments: [ 'CacheBundle\Entity\Category' ]
public: false
cache.repository.analytic_context:
class: 'AppBundle\Doctrine\ORM\BaseEntityRepository'
factory: [ '@doctrine.orm.default_entity_manager', 'getRepository' ]
arguments: [ 'CacheBundle\Entity\Analytic\AnalyticContext' ]
public: false
cache.validator.category_parent:
class: 'CacheBundle\Validator\Constraints\CategoryParentValidator'
arguments: [ '@cache.repository.category' ]
tags:
- { name: validator.constraint_validator }
cache.comment_manager:
class: 'CacheBundle\Comment\Manager\CommentManager'
arguments:
- '@doctrine.orm.default_entity_manager'
cache.document_content_extractor.basic:
class: 'CacheBundle\Document\Extractor\BasicDocumentContentExtractor'
arguments:
- "@=service('app.configuration').getParameter('notification.start_extract_length')"
- "@=service('app.configuration').getParameter('notification.context_extract_length')"
cache.document_content_extractor: '@cache.document_content_extractor.basic'
cache.analytic_factory:
class: 'CacheBundle\Service\Factory\Analytic\AnalyticFactory'
arguments:
- '@cache.repository.analytic_context'
File diff suppressed because one or more lines are too long
@@ -0,0 +1,88 @@
<?php
namespace CacheBundle\Security\Inspector;
use ApiBundle\Security\Inspector\AbstractInspector;
use CacheBundle\Entity\Analytic\Analytic;
use UserBundle\Entity\User;
/**
* Class AnalyticInspector
* @package CacheBundle\Security\Inspector
*/
class AnalyticInspector extends AbstractInspector
{
/**
* Return supported entity fqcn.
*
* @return string
*/
public static function supportedClass()
{
return Analytic::class;
}
/**
* Check that user can create specified entity.
*
* @param User $user A user who try to create entity.
* @param Analytic|object $entity A Entity instance.
*
* @return void
*/
protected function canCreate(User $user, $entity)
{
$this->addReasonIf(
"Can't create analytics 'cause you don't have permissions for it.",
! $user->getBillingSubscription()->getPlan()->isAnalytics()
);
}
/**
* Check that user can read specified entity.
*
* @param User $user A user who try to create entity.
* @param Analytic|object $entity A Entity instance.
*
* @return void
*/
protected function canRead(User $user, $entity)
{
// todo implement check
}
/**
* Check that user can update specified entity.
*
* @param User $user A user who try to create entity.
* @param Analytic|object $entity A Entity instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function canUpdate(User $user, $entity)
{
// todo implement check
}
/**
* Check that user can delete specified entity.
*
* @param User $user A user who try to create entity.
* @param Analytic|object $entity A Entity instance.
*
* @return void
*/
protected function canDelete(User $user, $entity)
{
$this
->addReasonIf(
"Can't delete analytic owned by other user.",
! $entity->isOwnedBy($user)
);
}
}
@@ -0,0 +1,101 @@
<?php
namespace CacheBundle\Security\Inspector;
use ApiBundle\Security\Inspector\AbstractInspector;
use CacheBundle\Entity\Category;
use UserBundle\Entity\User;
/**
* Class CategoryInspector
* @package CacheBundle\Security\Inspector
*/
class CategoryInspector extends AbstractInspector
{
/**
* Return supported entity fqcn.
*
* @return string
*/
public static function supportedClass()
{
return Category::class;
}
/**
* Check that user can create specified entity.
*
* @param User $user A user who try to create entity.
* @param Category|object $entity A Entity instance.
*
* @return void
*/
protected function canCreate(User $user, $entity)
{
$this->addReasonIf(
"Can't create category for other user.",
! $entity->isOwnedBy($user)
);
}
/**
* Check that user can read specified entity.
*
* @param User $user A user who try to create entity.
* @param Category|object $entity A Entity instance.
*
* @return void
*/
protected function canRead(User $user, $entity)
{
$this->addReasonIf(
"Can't read category owned by other user.",
! $entity->isOwnedBy($user)
);
}
/**
* Check that user can update specified entity.
*
* @param User $user A user who try to create entity.
* @param Category|object $entity A Entity instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function canUpdate(User $user, $entity)
{
$this
->addReasonIf(
"Can't update category owned by other user.",
! $entity->isOwnedBy($user)
)
->addReasonIf(
"Can't update internal category.",
$entity->isInternal()
);
}
/**
* Check that user can delete specified entity.
*
* @param User $user A user who try to create entity.
* @param Category|object $entity A Entity instance.
*
* @return void
*/
protected function canDelete(User $user, $entity)
{
$this
->addReasonIf(
"Can't delete category owned by other user.",
! $entity->isOwnedBy($user)
)
->addReasonIf(
"Can't delete internal category.",
$entity->isInternal()
);
}
}
@@ -0,0 +1,92 @@
<?php
namespace CacheBundle\Security\Inspector;
use ApiBundle\Security\Inspector\AbstractInspector;
use CacheBundle\Entity\Comment;
use UserBundle\Entity\User;
/**
* Class CommentInspector
* @package CacheBundle\Security\Inspector
*/
class CommentInspector extends AbstractInspector
{
/**
* Return supported entity fqcn.
*
* @return string
*/
public static function supportedClass()
{
return Comment::class;
}
/**
* Check that user can create specified entity.
*
* @param User $user A user who try to create entity.
* @param Comment|object $entity A Entity instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function canCreate(User $user, $entity)
{
$this->addReasonIf(
"Can't create comment for other user.",
$entity->getAuthor()->getId() !== $user->getId()
);
}
/**
* Check that user can read specified entity.
*
* @param User $user A user who try to create entity.
* @param Comment|object $entity A Entity instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function canRead(User $user, $entity)
{
// Do nothing.
}
/**
* Check that user can update specified entity.
*
* @param User $user A user who try to create entity.
* @param Comment|object $entity A Entity instance.
*
* @return void
*/
protected function canUpdate(User $user, $entity)
{
$this
->addReasonIf(
"Can't update comment owned by other user.",
$entity->getAuthor()->getId() !== $user->getId()
);
}
/**
* Check that user can delete specified entity.
*
* @param User $user A user who try to create entity.
* @param Comment|object $entity A Entity instance.
*
* @return void
*/
protected function canDelete(User $user, $entity)
{
$this
->addReasonIf(
"Can't delete comment owned by other user.",
$entity->getAuthor()->getId() !== $user->getId()
);
}
}
@@ -0,0 +1,95 @@
<?php
namespace CacheBundle\Security\Inspector;
use ApiBundle\Security\Inspector\AbstractInspector;
use CacheBundle\Entity\Feed\AbstractFeed;
use CacheBundle\Entity\Feed\ClipFeed;
use CacheBundle\Entity\Feed\QueryFeed;
use UserBundle\Entity\User;
/**
* Class FeedInspector
* @package CacheBundle\Security\Inspector
*/
class FeedInspector extends AbstractInspector
{
/**
* Return supported entity fqcn.
*
* @return string|string[]
*/
public static function supportedClass()
{
return [ QueryFeed::class, ClipFeed::class ];
}
/**
* Check that user can create specified entity.
*
* @param User $user A user who try to create entity.
* @param AbstractFeed|object $entity A Entity instance.
*
* @return void
*/
protected function canCreate(User $user, $entity)
{
$this->addReasonIf(
"Can't create feed for other user.",
! $entity->isOwnedBy($user)
);
}
/**
* Check that user can read specified entity.
*
* @param User $user A user who try to create entity.
* @param AbstractFeed|object $entity A Entity instance.
*
* @return void
*/
protected function canRead(User $user, $entity)
{
$this->addReasonIf(
"Can't read feed owned by other user.",
! $entity->isOwnedBy($user)
);
}
/**
* Check that user can update specified entity.
*
* @param User $user A user who try to create entity.
* @param AbstractFeed|object $entity A Entity instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function canUpdate(User $user, $entity)
{
$this
->addReasonIf(
"Can't update feed owned by other user.",
! $entity->isOwnedBy($user)
);
}
/**
* Check that user can delete specified entity.
*
* @param User $user A user who try to create entity.
* @param AbstractFeed|object $entity A Entity instance.
*
* @return void
*/
protected function canDelete(User $user, $entity)
{
$this
->addReasonIf(
"Can't delete feed owned by other user.",
! $entity->isOwnedBy($user)
);
}
}
@@ -0,0 +1,124 @@
<?php
namespace CacheBundle\Security\Inspector;
use ApiBundle\Security\Inspector\AbstractInspector;
use CacheBundle\Entity\SourceList;
use UserBundle\Entity\User;
/**
* Class SourceListInspector
* @package CacheBundle\Security\Inspector
*/
class SourceListInspector extends AbstractInspector
{
const SHARE = 'share';
const UNSHARE = 'unshare';
/**
* Return supported entity fqcn.
*
* @return string
*/
public static function supportedClass()
{
return [ SourceList::class ];
}
/**
* Checks that given user can make given action with specified entity.
*
* @param User $user A User entity instance.
* @param object|SourceList $entity A Entity instance or array of instances.
* @param string $action Action name.
*
* @return string[] Array of restriction reasons.
*/
public function inspect(User $user, $entity, $action)
{
parent::inspect($user, $entity, $action);
if ($action === self::SHARE) {
$this->addReasonIf(
"Can't share source list owned by other user.",
! $entity->isOwnedBy($user)
);
} elseif ($action === self::UNSHARE) {
$this->addReasonIf(
"Can't unshare source list owned by other user.",
! $entity->isOwnedBy($user)
);
}
return $this->reasons;
}
/**
* Check that user can create specified entity.
*
* @param User $user A user who try to create entity.
* @param SourceList|object $entity A Entity instance.
*
* @return void
*/
protected function canCreate(User $user, $entity)
{
$this->addReasonIf(
"Can't create feed for other user.",
! $entity->isOwnedBy($user)
);
}
/**
* Check that user can read specified entity.
*
* @param User $user A user who try to create entity.
* @param SourceList|object $entity A Entity instance.
*
* @return void
*/
protected function canRead(User $user, $entity)
{
$this->addReasonIf(
"Can't read feed owned by other user.",
! $entity->isOwnedBy($user)
);
}
/**
* Check that user can update specified entity.
*
* @param User $user A user who try to create entity.
* @param SourceList|object $entity A Entity instance.
*
* @return void
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function canUpdate(User $user, $entity)
{
$this
->addReasonIf(
"Can't update feed owned by other user.",
! $entity->isOwnedBy($user)
);
}
/**
* Check that user can delete specified entity.
*
* @param User $user A user who try to create entity.
* @param SourceList|object $entity A Entity instance.
*
* @return void
*/
protected function canDelete(User $user, $entity)
{
$this
->addReasonIf(
"Can't delete feed owned by other user.",
! $entity->isOwnedBy($user)
);
}
}
@@ -0,0 +1,181 @@
<?php
namespace CacheBundle\Service\Factory\Analytic;
use AppBundle\Doctrine\ORM\BaseEntityRepository;
use AppBundle\Exception\NotAllowedException;
use CacheBundle\DTO\AnalyticDTO;
use CacheBundle\Entity\Analytic\Analytic;
use CacheBundle\Entity\Analytic\AnalyticContext;
use CacheBundle\Entity\Feed\AbstractFeed;
use IndexBundle\Filter\FilterInterface;
use UserBundle\Entity\User;
use UserBundle\Enum\AppPermissionEnum;
/**
* Class AnalyticFactory
*
* @package CacheBundle\Service\Factory\Analytic
*/
class AnalyticFactory implements AnalyticFactoryInterface
{
/**
* @var BaseEntityRepository
*/
private $analyticRepository;
/**
* AnalyticFactory constructor.
*
* @param BaseEntityRepository $analyticRepository A internal analytic repository.
*/
public function __construct(BaseEntityRepository $analyticRepository)
{
$this->analyticRepository = $analyticRepository;
}
/**
* @param AnalyticDTO $dto A analytic dto instance.
* @param User $user User for which we create analytic.
*
* @return Analytic
*
* @throws \InvalidArgumentException If got invalid dto.
* @throws NotAllowedException If specified user can't create save.
*/
public function createAnalytic(AnalyticDTO $dto, User $user)
{
if (!$user->isAllowedTo(AppPermissionEnum::analytics())) {
throw new NotAllowedException($user, AppPermissionEnum::analytics());
}
$context = $this->createContext($dto);
return new Analytic($user, $context);
}
/**
* @param AnalyticDTO $dto A data required for creating analytic.
*
* @return AnalyticContext
*/
private function createContext(AnalyticDTO $dto)
{
if (!\app\a\allInstanceOf($dto->feeds, AbstractFeed::class)) {
throw new \InvalidArgumentException(sprintf(
'\'$dto->feeds\' should be an array of \'%s\' instances',
AbstractFeed::class
));
}
if (!\app\a\allInstanceOf($dto->filters, FilterInterface::class)) {
throw new \InvalidArgumentException(sprintf(
'\'$dto->filters\' should be an array of \'%s\' instances',
FilterInterface::class
));
}
if (!is_array($dto->rawFilters)) {
throw new \InvalidArgumentException('\'$dto->rawFilters\' should be an array');
}
$hash = $this->computeHash($dto->feeds, $dto->filters);
$analytic = $this->analyticRepository->find($hash);
if ($analytic === null) {
$analytic = new AnalyticContext(
$hash,
$dto->feeds,
$dto->filters,
$dto->rawFilters
);
}
return $analytic;
}
/**
* Compute hash for analytic.
*
* @param AbstractFeed[] $feeds Array of used feeds.
* @param FilterInterface[] $filters Array of used filters.
*
* @return string
*/
private function computeHash(array $feeds, array $filters)
{
$collectionIds = $this->getUniqueCollectionIds($feeds);
sort($collectionIds);
foreach ($filters as $filter) {
$filter->sort();
}
return md5(serialize($collectionIds) . serialize($filters));
}
/**
* @param AbstractFeed[] $feeds Collection of used feeds.
*
* @return string[]
*/
private function getUniqueCollectionIds(array $feeds)
{
$collectionIds = [];
foreach ($feeds as $feed) {
$collectionIds[$feed->getCollectionId()] = true;
}
return array_keys($collectionIds);
}
/**
* @param AnalyticDTO $dto
* @param User $user
* @param Analytic $oldAnalytic
* @return Analytic|AnalyticContext|object|null
*/
public function updateAnalytic(AnalyticDTO $dto, User $user, Analytic $oldAnalytic)
{
if (!$user->isAllowedTo(AppPermissionEnum::analytics())) {
throw new NotAllowedException($user, AppPermissionEnum::analytics());
}
if (!\app\a\allInstanceOf($dto->feeds, AbstractFeed::class)) {
throw new \InvalidArgumentException(sprintf(
'\'$dto->feeds\' should be an array of \'%s\' instances',
AbstractFeed::class
));
}
if (!\app\a\allInstanceOf($dto->filters, FilterInterface::class)) {
throw new \InvalidArgumentException(sprintf(
'\'$dto->filters\' should be an array of \'%s\' instances',
FilterInterface::class
));
}
if (!is_array($dto->rawFilters)) {
throw new \InvalidArgumentException('\'$dto->rawFilters\' should be an array');
}
$hash = $this->computeHash($dto->feeds, $dto->filters);
$analytic = $this->analyticRepository->find($hash);
if ($analytic === null) {
$analytic = new AnalyticContext(
$hash,
$dto->feeds,
$dto->filters,
$dto->rawFilters
);
/** @var $oldAnalytic $analytic */
$oldAnalytic->setContext($analytic);
}
return $oldAnalytic;
}
}
@@ -0,0 +1,39 @@
<?php
namespace CacheBundle\Service\Factory\Analytic;
use AppBundle\Exception\NotAllowedException;
use CacheBundle\DTO\AnalyticDTO;
use CacheBundle\Entity\Analytic\Analytic;
use UserBundle\Entity\User;
/**
* Interface AnalyticFactoryInterface
*
* Factory for creating saved analytics for specified users.
*
* @package CacheBundle\Service\Factory\Analytic
*/
interface AnalyticFactoryInterface
{
/**
* @param AnalyticDTO $dto A analytic dto instance.
* @param User $user User for which we create analytic.
*
* @return Analytic
*
* @throws \InvalidArgumentException If got invalid dto.
* @throws NotAllowedException If specified user can't create save.
*/
public function createAnalytic(AnalyticDTO $dto, User $user);
/**
* @param AnalyticDTO $dto
* @param User $user
* @param Analytic $analytic
* @return mixed
*/
public function updateAnalytic(AnalyticDTO $dto, User $user, Analytic $analytic);
}
@@ -0,0 +1,31 @@
<?php
namespace CacheBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* Class CategoryParent
*
* @Annotation
* @Target({"PROPERTY", "ANNOTATION"})
*
* @package CacheBundle\Validator\Constraints
*/
class CategoryParent extends Constraint
{
/**
* Returns whether the constraint can be put onto classes, properties or
* both.
*
* This method should return one or more of the constants
* Constraint::CLASS_CONSTRAINT and Constraint::PROPERTY_CONSTRAINT.
*
* @return string|array One or more constant values.
*/
public function getTargets()
{
return [ self::PROPERTY_CONSTRAINT ];
}
}
@@ -0,0 +1,74 @@
<?php
namespace CacheBundle\Validator\Constraints;
use CacheBundle\Entity\Category;
use CacheBundle\Repository\CategoryRepository;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\RuntimeException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* Class CategoryParentValidator
* @package CacheBundle\Validator\Constraints
*/
class CategoryParentValidator extends ConstraintValidator
{
/**
* @var CategoryRepository
*/
private $repository;
/**
* CategoryParentValidator constructor.
*
* @param CategoryRepository $repository A CategoryRepository instance.
*/
public function __construct(CategoryRepository $repository)
{
$this->repository = $repository;
}
/**
* Checks if the passed value is valid.
*
* @param mixed $value The value that should be validated.
* @param Constraint $constraint The constraint for the validation.
*
* @return void
*/
public function validate($value, Constraint $constraint)
{
if (! $constraint instanceof CategoryParent) {
throw new UnexpectedTypeException($constraint, CategoryParent::CLASS_CONSTRAINT);
}
// Check current object.
$current = $this->context->getObject();
if (! $current instanceof Category) {
throw new RuntimeException('This validator works only with categories.');
}
// We don't make any checks if current category is new.
if ($current->getId() === null) {
return;
}
// Check that we try to put category inside it self.
if ($value instanceof Category) {
$currentId = $current->getId();
$valueId = $value->getId();
if ($valueId === $currentId) {
$this->context->addViolation('Try to place category inside itself.');
}
// Check that we try to put category inside one of it childes.
if ($this->repository->isChildOf($valueId, $currentId)) {
$this->context->addViolation('Try to place category inside it child.');
}
}
}
}