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
+338
View File
@@ -0,0 +1,338 @@
<?php
namespace Common\Context;
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\PyStringNode;
use Common\Util\DatabaseHelper;
use Common\Util\Index\InternalSourceConnection;
use Common\Util\Processor\DataProcessor;
use Common\Util\Index\ExternalIndexConnection;
use Common\Util\Index\InternalIndexConnection;
use Common\Util\Matcher\AppMatcher;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\ORM\EntityManagerInterface;
use IndexBundle\Fixture\Executor\Factory\IndexFixtureExecutorFactory;
use IndexBundle\Fixture\Executor\Factory\IndexFixtureExecutorFactoryInterface;
use IndexBundle\Fixture\Loader\IndexFixtureLoader;
use Seld\JsonLint\JsonParser;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class AbstractContext
* Base class for all application test context's.
*
* @package Common\Context
*/
class AbstractContext extends \PHPUnit_Framework_Assert implements Context
{
use DatabaseContextTrait,
IndexContextTrait;
/**
* True if test running with debug.
*
* @var boolean
*/
protected $debug;
/**
* @var ContainerInterface
*/
protected $container;
/**
* Path to data fixtures directory.
*
* @var string
*/
protected $fixturesDir;
/**
* @var DataProcessor
*/
protected $processor;
/**
* @var IndexFixtureExecutorFactoryInterface
*/
protected $indexExecutorFactory;
/**
* True if all indices initialized.
*
* @var boolean
*/
private static $indexInitialized = false;
/**
* Setup database schemas.
*
* @BeforeSuite
*
* @return void
*/
public static function setup()
{
if (strtolower(trim(getenv('WITHOUT_CLEAR'))) !== 'true') {
system('./bin/console --env=test cache:clear');
system('./bin/console --env=test doctrine:database:create --if-not-exists -n');
system('./bin/console --env=test doctrine:schema:update --force -n');
}
}
/**
* Clear external index and load fixtures before scenario.
*
* @BeforeScenario @external-index-fixtures
*
* @return void
*/
public function setupExternalIndexFixtures()
{
$this->createIndexes();
echo 'Purge external index ... ';
$this->externalIndex->purge();
echo 'done'. PHP_EOL;
echo 'Load indices fixtures: '. PHP_EOL;
$loader = new IndexFixtureLoader($this->container);
$loader->loadFromDirectory($this->fixturesDir);
$this->indexExecutorFactory->external($this->externalIndex->getIndex())
->setLogger(function ($message) {
echo " > {$message}". PHP_EOL;
})
->execute($loader->getFixtures());
// Wait to insure that all fixtures was indexed.
sleep(2);
}
/**
* Clear external index and load fixtures before scenario.
*
* @BeforeScenario @internal-index-fixtures
*
* @return void
*/
public function setupInternalIndexFixtures()
{
$this->createIndexes();
echo 'Purge internal index ... ';
$this->internalIndex->purge();
echo 'done'. PHP_EOL;
echo 'Load indices fixtures: '. PHP_EOL;
$loader = new IndexFixtureLoader($this->container);
$loader->loadFromDirectory($this->fixturesDir);
$this->indexExecutorFactory->internal($this->internalIndex->getIndex())
->setLogger(function ($message) {
echo " > {$message}". PHP_EOL;
})
->execute($loader->getFixtures());
// Wait to insure that all fixtures was indexed.
sleep(2);
}
/**
* Clear external index and load fixtures before scenario.
*
* @BeforeScenario @source-index-fixtures
*
* @return void
*/
public function setupSourceIndexFixtures()
{
//
// Remove source_update.date
//
// NOTICE: Insure that you don't run test in production :)
//
unlink(realpath(__DIR__ . '/../../../var/source_update.date'));
$this->createIndexes();
$this->setupExternalIndexFixtures();
echo 'Purge source index ... ';
$this->sourceIndex->purge();
echo 'done'. PHP_EOL;
echo 'Load indices fixtures: '. PHP_EOL;
$loader = new IndexFixtureLoader($this->container);
$loader->loadFromDirectory($this->fixturesDir);
$this->indexExecutorFactory->source($this->sourceIndex->getIndex())
->setLogger(function ($message) {
echo " > {$message}". PHP_EOL;
})
->execute($loader->getFixtures());
// Wait to insure that all fixtures was indexed.
sleep(2);
}
/**
* Create indexes if we want to upload index fixtures.
*
* @return void
*/
public function createIndexes()
{
if (! self::$indexInitialized
&& (strtolower(trim(getenv('WITHOUT_CLEAR'))) !== 'true')) {
$this->externalIndex->setup();
$this->internalIndex->setup();
$this->sourceIndex->setup();
self::$indexInitialized = true;
}
}
/**
* @param ContainerInterface $container A ContainerInterface instance.
* @param string $fixturesDir Path to fixtures directory.
*/
public function __construct(ContainerInterface $container, $fixturesDir)
{
$this->debug = getenv('DEBUG') !== false;
$this->container = $container;
$this->fixturesDir = realpath($fixturesDir);
if ($container->getParameter('kernel.environment') !== 'test') {
$message = 'You should run test in test environment /:|';
throw new \InvalidArgumentException($message);
}
// Create database helper.
// See Common\Context\DatabaseContextTrait
$this->dataBaseHelper = new DatabaseHelper($container->get('doctrine'));
// Get serializer metadata for all entities.
// We make it for simplification testing process. With this information
// we can make more powerful expanders and matchers which help as to
// write less but make more.
/** @var EntityManagerInterface $em */
$em = $this->container->get('doctrine.orm.default_entity_manager');
// Get all available entity fqcn's.
$fqcnList = array_map(function (ClassMetadata $metadata) {
return $metadata->getName();
}, $em->getMetadataFactory()->getAllMetadata());
$entities = $this->processEntityMetadata($em, $fqcnList);
// Register all entities.
AppMatcher::registerEntities($entities);
$this->processor = new DataProcessor($container);
// Decorate indices connections.
$this->externalIndex = new ExternalIndexConnection(
$this->get('index.external')
);
$this->internalIndex = new InternalIndexConnection(
$this->get('index.articles')
);
$this->sourceIndex = new InternalSourceConnection(
$this->get('index.sources')
);
$this->indexExecutorFactory = new IndexFixtureExecutorFactory();
}
/**
* @When /^(?:|I )[Ww]ait (?P<milliseconds>\d+) millisecond(?: until| for)?[\w\s]*$/
*
* @param integer $milliseconds Seconds count.
*
* @return void
*/
public function wait($milliseconds)
{
usleep($milliseconds * 1000);
}
/**
* Gets a service.
*
* @param string $id The service identifier.
*
* @return object The associated service.
*/
protected function get($id)
{
return $this->container->get($id);
}
/**
* Gets a parameter.
*
* @param string $name The parameter name.
*
* @return mixed The parameter value.
*/
protected function getParameter($name)
{
return $this->container->getParameter($name);
}
/**
* Match value against specified pattern.
*
* @param mixed $value Matched value.
* @param mixed $pattern Pattern.
* @param string $error Occurred error.
*
* @return boolean
*/
protected function match($value, $pattern, &$error)
{
$value = $this->processor->process($value);
$pattern = preg_replace('/\\s{2,}/', '', $pattern);
// Lint pattern only if it contains json.
if (($pattern[0] === '{') || ($pattern[0] === '[')) {
// Lint pattern.
$lint = new JsonParser();
$exception = $lint->lint($pattern);
if ($exception !== null) {
throw new \RuntimeException('Pattern lint: ' . $exception->getMessage());
}
}
// Process expressions between ##.
$pattern = $this->processor->process($pattern);
return AppMatcher::match($value, $pattern, $error);
}
/**
* Lint json.
*
* @param PyStringNode|string $json Json to lint.
*
* @return void
*/
protected function lintJson($json)
{
if ($json instanceof PyStringNode) {
$json = $json->getRaw();
}
$linter = new JsonParser();
$exception = $linter->lint($json);
if ($exception !== null) {
throw $exception;
}
}
}
@@ -0,0 +1,231 @@
<?php
namespace Common\Context;
use ApiBundle\Entity\NormalizableEntityInterface;
use ApiBundle\Serializer\Metadata\Metadata;
use AppBundle\Utils\Purger\TruncateORMPurger;
use Behat\Gherkin\Node\TableNode;
use Common\Util\DatabaseHelper;
use Common\Util\Metadata\EntityMetadata;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader;
/**
* Class DatabaseContextTrait
* Contains steps definitions for working with database.
*
* @package Common\Context
*/
trait DatabaseContextTrait
{
/**
* @var DatabaseHelper
*/
private $dataBaseHelper;
/**
* Clear database and load fixtures before scenario.
*
* @BeforeScenario @db-fixtures
*
* @return void
*/
public function setupDbFixtures()
{
/** @var EntityManagerInterface $em */
$em = $this->get('doctrine.orm.entity_manager');
echo 'Purge database ... ';
$purger = new TruncateORMPurger(new ORMPurger($em));
$purger->purge();
echo 'done'. PHP_EOL;
$loader = new ContainerAwareLoader($this->container);
$loader->loadFromDirectory($this->fixturesDir);
echo 'Load database fixtures:'. PHP_EOL;
$executor = new ORMExecutor($em);
$executor
->setLogger(function ($message) {
echo " > {$message}". PHP_EOL;
});
$executor->execute($loader->getFixtures(), true);
}
/**
* @Then /^(?:|[Dd]atabase )[Hh]as entity (?P<name>.+)$/
*
* @param string $name Entity short name like AppBundle:Entity or FQCN.
* @param TableNode $table Search parameters in table format.
*
* @return void
*/
public function hasEntity($name, TableNode $table)
{
$params = [];
$tableData = $table->getTable();
foreach ($tableData as $row) {
$params[current($row)] = next($row);
}
$entity = $this->dataBaseHelper->getEntity($name, $params);
self::assertNotNull(
$entity,
"Can't find entity {$name} with parameters " . PHP_EOL
. json_encode($params, JSON_PRETTY_PRINT)
);
}
/**
* @Then /^(?:|[Dd]atabase )[Hh]as (?P<count>\d+) entity (?P<name>.+)$/
* @Then /?[Dd]on't has entity (?P<name>.+)$?
*
* @param string $name Entity short name like AppBundle:Entity or FQCN.
* @param integer $count Expected entities count.
* @param TableNode $table Search parameters in table format.
*
* @return void
*/
public function hasEntities($name, $count, TableNode $table)
{
$params = [];
$tableData = $table->getTable();
foreach ($tableData as $row) {
$params[current($row)] = next($row);
}
$entities = $this->dataBaseHelper->getEntities($name, $params);
self::assertCount(
(int) $count,
$entities,
"Can't find {$count} entity {$name} with parameters " . PHP_EOL
. json_encode($params, JSON_PRETTY_PRINT) .PHP_EOL .
'Actually found: '. count($entities)
);
}
/**
* @Then /^[Ii] want to delete entity (?P<name>.+)$/
*
* @param string $name Entity short name like AppBundle:Entity or FQCN.
* @param TableNode $table Search parameters in table format.
*
* @return void
*/
public function deleteEntity($name, TableNode $table)
{
$params = [];
$tableData = $table->getTable();
foreach ($tableData as $row) {
$params[current($row)] = next($row);
}
$this->dataBaseHelper->deleteEntity($name, $params);
}
/**
* @Then /^(?:|[Dd]atabase )[Dd]on't has entity (?P<name>.+)$/
* @Then /?[Hh]as entity (?P<name>.+)$?
*
* @param string $name Entity short name like AppBundle:Entity or FQCN.
* @param TableNode $table Search parameters ikn table format.
*
* @return void
*/
public function entityNotExists($name, TableNode $table)
{
$params = [];
$tableData = $table->getTable();
foreach ($tableData as $row) {
$params[current($row)] = next($row);
}
$entities = $this->dataBaseHelper->getEntities($name, $params);
self::assertCount(
0,
$entities,
"Entity {$name} with parameters " . PHP_EOL
. json_encode($params, JSON_PRETTY_PRINT) . PHP_EOL .'exists!'
);
}
/**
* @param EntityManagerInterface $em A EntityManagerInterface instance.
* @param array $fqcnList List of available fqcn's.
*
* @return array|mixed
*/
protected function processEntityMetadata(EntityManagerInterface $em, array $fqcnList)
{
$entities = [];
foreach ($fqcnList as $fqcn) {
$reflection = new \ReflectionClass($fqcn);
if ($reflection->implementsInterface(NormalizableEntityInterface::class)) {
$name = \app\c\entityFqcnToShort($fqcn);
if ($reflection->isAbstract()) {
$entities[$name] = new EntityMetadata($this->processAbstractMetadata($em, $reflection));
} else {
/** @var NormalizableEntityInterface $entity */
$entity = $reflection->newInstanceWithoutConstructor();
$entities[$name] = new EntityMetadata($entity->getMetadata());
}
}
}
return $entities;
}
/**
* @param EntityManagerInterface $em A EntityManagerInterface
* instance.
* @param \ReflectionClass $reflection A ReflectionClass instance.
*
* @return Metadata
*/
protected function processAbstractMetadata(
EntityManagerInterface $em,
\ReflectionClass $reflection
) {
/** @var ClassMetadataInfo $doctrineMetadata */
$doctrineMetadata = $em->getClassMetadata($reflection->getName());
$map = $doctrineMetadata->discriminatorMap;
if (! is_array($map) || (count($map) === 0)) {
// Parsed abstract class don't has discriminator column.
$message = 'Abstract class without discriminator column not allowed';
throw new \InvalidArgumentException($message);
}
$metadata = new Metadata($reflection->getName());
$metadataList = array_map(function ($fqcn) use ($em) {
$reflection = new \ReflectionClass($fqcn);
if ($reflection->isAbstract()) {
/** @var ClassMetadataInfo $doctrineMetadata */
$doctrineMetadata = $em->getClassMetadata($fqcn);
$map = $doctrineMetadata->discriminatorMap;
return $this->processAbstractMetadata($em, $map);
}
/** @var NormalizableEntityInterface $entity */
$entity = $reflection->newInstanceWithoutConstructor();
return $entity->getMetadata();
}, $map);
return $metadata->admixList($metadataList);
}
}
+135
View File
@@ -0,0 +1,135 @@
<?php
namespace Common\Context;
use Behat\Gherkin\Node\TableNode;
use Common\Util\Index\ExternalIndexConnection;
use Common\Util\Index\InternalIndexConnection;
use Common\Util\Index\InternalSourceConnection;
use Common\Util\Index\TestIndexConnectionInterface;
/**
* Class IndexContextTrait
* Contains steps definitions for working with indices.
*
* @package Common\Context
*/
trait IndexContextTrait
{
/**
* @var ExternalIndexConnection
*/
protected $externalIndex;
/**
* @var InternalIndexConnection
*/
protected $internalIndex;
/**
* @var InternalSourceConnection
*/
protected $sourceIndex;
/**
* @Transform /^([Ee]xternal|[Ii]nternal|[Ss]ource)$/
*
* @param string $index Index name.
*
* @return TestIndexConnectionInterface
*/
public function getConnectionByName($index)
{
$index = strtolower($index);
switch ($index) {
case 'external':
return $this->externalIndex;
case 'internal':
return $this->internalIndex;
case 'source':
return $this->sourceIndex;
}
throw new \InvalidArgumentException("Unknown index '{$index}'");
}
/**
* Json parameters should have only necessary fields. Over will be auto
* generated.
*
* @Given /^(?:I add|[Hh]as) new document (?:in|to) (?P<connection>(external|internal|source)) index$/
*
* @param TestIndexConnectionInterface $index A TestIndexConnectionInterface
* instance.
* @param TableNode $table A TableNode instance.
*
* @return void
*/
public function indexDocument(
TestIndexConnectionInterface $index,
TableNode $table
) {
/** @var AbstractContext $this */
$document = $index->createDocument();
$params = [];
$tableData = $table->getTable();
foreach ($tableData as $row) {
$params[current($row)] = $this->processor->process(next($row));
}
foreach ($params as $name => $value) {
$document[$name] = $value;
}
$index->index($document);
// Wait to insure that all fixtures was indexed.
usleep(100000);
}
/**
* @Then /^(?P<connection>([Ee]xternal|[Ii]nternal|[Ss]ource)) index has (?P<count>\d+) document[s]?$/
*
* @param TestIndexConnectionInterface $connection A
* TestIndexConnectionInterface
* instance.
* @param integer $count A expected documents
* count.
* @param TableNode $table A TableNode instance.
*
* @return void
*/
public function hasDocuments(
TestIndexConnectionInterface $connection,
$count,
TableNode $table
) {
/** @var AbstractContext $this */
$tableData = $table->getTable();
$factory = $connection->getFilterFactory();
$filters = [];
foreach ($tableData as $row) {
$field = current($row);
$type = next($row);
$value = next($row);
if ($type === 'in') {
$value = array_filter(array_map('trim', explode(',', $value)));
}
$filters[] =
$factory->{$type}($field, $this->processor->process($value));
}
$results = $connection->createRequestBuilder()
->setFilters($filters)
->build()
->execute();
self::assertCount((int) $count, $results);
}
}
@@ -0,0 +1,87 @@
<?php
namespace Common\Util\Converter;
/**
* Class DateConverter
* @package Common\Util\Converter
*/
class DateConverter
{
/**
* Map between date format and proper regular patterns.
*
* @var string[]
*/
private static $datePatternsMap = [
'Y-m-d' => '\d{4}-\d{2}-\d{2}',
'Y-m-d H:i:s' => '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}',
'Y-m-d\TH:i:sP' => '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(:?(:?\+|\-)\d{2}:\d{2}|\w)',
];
/**
* Cache of formats for specified dates.
*
* @var string
*/
private static $dateFormatCache = [];
/**
* Check that specified string can be converted to \DateTime instance.
*
* @param string $date Date string.
*
* @return boolean
*/
public static function can($date)
{
return self::getFormat($date) !== null;
}
/**
* @param string $date Date string.
*
* @return \DateTime|false
*/
public static function convert($date)
{
$format = self::getFormat($date);
if ($format === null) {
throw new \InvalidArgumentException('Invalid date '. $date);
}
return date_create_from_format($format, $date);
}
/**
* Get proper date format for specified date string.
*
* @param string $date Date string.
*
* @return string|null
*/
private static function getFormat($date)
{
if (! is_string($date)) {
return false;
}
if (! isset(self::$dateFormatCache[$date])) {
self::$dateFormatCache[$date] = null;
// Check specified date against all available patterns and find
// proper format.
foreach (self::$datePatternsMap as $format => $pattern) {
if ((preg_match('/^' . $pattern . '$/', $date) === 1)
&& (date_create_from_format($format, $date) !== false)) {
self::$dateFormatCache[$date] = $format;
break;
}
}
}
return self::$dateFormatCache[$date];
}
}
+124
View File
@@ -0,0 +1,124 @@
<?php
namespace Common\Util;
use Common\Util\Converter\DateConverter;
use Doctrine\Bundle\DoctrineBundle\Registry;
/**
* Class DatabaseHelper
* Check database status.
*
* @package Common\Util
*/
class DatabaseHelper
{
/**
* @var Registry
*/
private $registry;
/**
* DatabaseHelper constructor.
*
* @param Registry $registry A Registry instance.
*/
public function __construct(Registry $registry)
{
$this->registry = $registry;
}
/**
* Check that given entity with specified parameters are exists in our
* database.
*
* @param string $name Entity short name like AppBundle:Entity or FQCN.
* @param array $params Entity parameters.
*
* @return array
*/
public function getEntities($name, array $params)
{
// Process parameters.
$params = $this->parseParams($params);
return $this->registry->getRepository($name)->findBy($params);
}
/**
* Remove some entities from BD by parameters
*
* @param string $name Entity short name like AppBundle:Entity or FQCN.
* @param array $params Entity parameters.
*
* @return void
*/
public function deleteEntity($name, array $params)
{
// Process parameters.
$params = $this->parseParams($params);
$entities = $this->registry->getRepository($name)->findBy($params);
$em = $this->registry->getEntityManager();
foreach ($entities as $entity) {
$em->remove($entity);
}
$em->flush();
}
/**
* Check that given entity with specified parameters are exists in our
* database.
*
* @param string $name Entity short name like AppBundle:Entity or FQCN.
* @param array $params Entity parameters.
*
* @return object|null
*/
public function getEntity($name, array $params)
{
// Process parameters.
$params = $this->parseParams($params);
return $this->registry->getRepository($name)->findOneBy($params);
}
/**
* Parse raw parameters.
*
* @param array $params Entity parameters.
*
* @return array
*/
private function parseParams(array $params)
{
return array_map(function ($parameter) {
$origin = trim($parameter);
$buf = strtolower($origin);
switch (true) {
// Parameter is valid numerical value convert it to float or
// integer.
case is_numeric($buf):
if (strpos($buf, '.') !== false) {
return (float) $buf;
}
return (int) $buf;
case DateConverter::can($buf):
return DateConverter::convert($buf);
case in_array($origin, \DateTimeZone::listIdentifiers(), true):
return new \DateTimeZone($origin);
// Parameter contains boolean value.
case ($buf === 'true') || ($buf === 'false'):
return $buf === 'true';
}
return $parameter;
}, $params);
}
}
@@ -0,0 +1,270 @@
<?php
namespace Common\Util\Index;
use AppBundle\AdvancedFilters\AFResolverInterface;
use AppBundle\Response\SearchResponseInterface;
use IndexBundle\Aggregation\AggregationFacadeInterface;
use IndexBundle\Aggregation\Factory\AggregationFactoryInterface;
use IndexBundle\Index\External\ExternalIndexInterface;
use IndexBundle\Index\IndexInterface;
use IndexBundle\Index\Internal\InternalIndexInterface;
use IndexBundle\Index\Source\SourceIndexInterface;
use IndexBundle\Index\Strategy\IndexStrategyInterface;
use IndexBundle\Model\DocumentInterface;
use IndexBundle\SearchRequest\SearchRequestBuilderInterface;
use IndexBundle\SearchRequest\SearchRequestInterface;
/**
* Class AbstractTestIndexConnection
*
* @package Common\Util\Index
*/
abstract class AbstractTestIndexConnection implements TestIndexConnectionInterface
{
/**
* @var InternalIndexInterface
*/
private $index;
/**
* AbstractIndexConnection constructor.
*
* @param InternalIndexInterface $index A InternalIndexInterface interface.
*/
public function __construct(InternalIndexInterface $index)
{
$this->index = $index;
}
/**
* Update specified document.
*
* Make partial update so in data must be placed only changed properties.
*
* @param string|integer $id Updated document id.
* @param array $data Array of changed data where key is property
* name and value is new property value.
*
* @return void
*/
public function update($id, array $data)
{
$this->index->update($id, $data);
}
/**
* Update array of documents.
*
* Make partial update so for each document id we should place only changed
* property.
*
* @param array $config Array of arrays where key is updated document id and
* value is array of updated fields same as $data in
* `update` method.
*
* @return void
*/
public function updateBulk(array $config)
{
$this->index->updateBulk($config);
}
/**
* Update array of documents with filtering.
*
* Make partial update so for each document id we should place only changed
* property.
*
* @param SearchRequestInterface $request A SearchRequestInterface instance.
* @param string $script Updating script.
* @param array $params Script parameters.
*
* @return void
*/
public function updateByQuery(SearchRequestInterface $request, $script, array $params = [])
{
$this->index->updateByQuery($request, $script, $params);
}
/**
* Remove document by specified id or array of ids.
*
* @param string|string[] $id Document id or array of document ids.
*
* @return void
*/
public function remove($id)
{
$this->index->remove($id);
}
/**
* Search information in index.
*
* @param SearchRequestInterface $request Internal representation of search
* request.
*
* @return SearchResponseInterface
*/
public function search(SearchRequestInterface $request)
{
return $this->index->search($request);
}
/**
* Fetch all relevant documents.
*
* @param SearchRequestInterface $request Internal representation of search
* request.
*
* @return \Traversable
*/
public function fetchAll(SearchRequestInterface $request)
{
return $this->index->fetchAll($request);
}
/**
* Create search request builder for this index connection.
*
* @return SearchRequestBuilderInterface
*/
public function createRequestBuilder()
{
return $this->index->createRequestBuilder();
}
/**
* Get filter factory instance.
*
* @return \IndexBundle\Filter\Factory\FilterFactoryInterface
*/
public function getFilterFactory()
{
return $this->index->getFilterFactory();
}
/**
* Get aggregation factory instance
*
* @return AggregationFactoryInterface
*/
public function getAggregationFactory()
{
return $this->index->getAggregationFactory();
}
/**
* Get aggregation instance
*
* @return AggregationFacadeInterface
*/
public function getAggregation()
{
return $this->index->getAggregation();
}
/**
* Return advanced filters aggregator.
*
* @return AFResolverInterface
*/
public function getAFResolver()
{
return $this->index->getAFResolver();
}
/**
* Get strategy used by this index.
*
* @return IndexStrategyInterface
*/
public function getStrategy()
{
return $this->index->getStrategy();
}
/**
* Create new index.
*
* @param array $mapping Index mapping.
* @param array $settings Index settings.
*
* @return void
*/
public function createIndex(array $mapping, array $settings = [])
{
if ($this->index instanceof InternalIndexInterface) {
$this->index->createIndex($mapping, $settings);
} else {
throw new \LogicException('Can\'t create index on '. get_class($this->index));
}
}
/**
* Index given document or array of documents.
*
* @param DocumentInterface|DocumentInterface[] $data DocumentInterface instance
* or array of instances.
*
* @return void
*/
public function index($data)
{
if ($this->index instanceof InternalIndexInterface) {
$this->index->index($data);
} else {
throw new \LogicException('Can\'t index documents on '. get_class($this->index));
}
}
/**
* Purge index.
*
* @return void
*/
public function purge()
{
if ($this->index instanceof InternalIndexInterface) {
$this->index->purge();
} else {
throw new \LogicException('Can\'t purge index on '. get_class($this->index));
}
}
/**
* Get documents by it ids.
*
* @param integer|integer[] $ids Array of document ids or single id.
* @param string|string[] $fields Array of requested fields of single
* field.
*
* @return \IndexBundle\Model\DocumentInterface[]
*/
public function get($ids, $fields = [])
{
return $this->index->get($ids, $fields);
}
/**
* Check that specified documents is exists.
*
* @param integer|array $ids Array of document ids or single id.
*
* @return array Contains all ids which not found in index.
*/
public function has($ids)
{
return $this->index->has($ids);
}
/**
* @return IndexInterface|InternalIndexInterface|ExternalIndexInterface|SourceIndexInterface
*/
public function getIndex()
{
return $this->index;
}
}
@@ -0,0 +1,53 @@
<?php
namespace Common\Util\Index;
use IndexBundle\Index\External\ExternalIndexInterface;
use IndexBundle\Model\Generator\ExternalDocumentGenerator;
use IndexBundle\Model\DocumentInterface;
use IndexBundle\Util\Initializer\ExternalIndexInitializer;
/**
* Class ExternalIndexConnection
*
* @package Common\Util\Index
*/
class ExternalIndexConnection extends AbstractTestIndexConnection
{
/**
* @var ExternalDocumentGenerator
*/
private $documentGenerator;
/**
* ExternalIndexConnection constructor.
*
* @param ExternalIndexInterface $index A ExternalIndexInterface interface.
*/
public function __construct(ExternalIndexInterface $index)
{
parent::__construct($index);
$this->documentGenerator = new ExternalDocumentGenerator();
}
/**
* Setup external index.
*
* @return void
*/
public function setup()
{
ExternalIndexInitializer::initialize($this);
}
/**
* Create new document for this index.
*
* @return DocumentInterface
*/
public function createDocument()
{
return $this->documentGenerator->generate();
}
}
@@ -0,0 +1,52 @@
<?php
namespace Common\Util\Index;
use IndexBundle\Index\Internal\InternalIndexInterface;
use IndexBundle\Model\Generator\InternalDocumentGenerator;
use IndexBundle\Model\DocumentInterface;
use IndexBundle\Util\Initializer\InternalIndexInitializer;
/**
* Class InternalIndexConnection
* @package Common\Util\Index
*/
class InternalIndexConnection extends AbstractTestIndexConnection
{
/**
* @var InternalDocumentGenerator
*/
private $documentGenerator;
/**
* ExternalIndexConnection constructor.
*
* @param InternalIndexInterface $index A InternalIndexInterface interface.
*/
public function __construct(InternalIndexInterface $index)
{
parent::__construct($index);
$this->documentGenerator = new InternalDocumentGenerator();
}
/**
* Setup internal index.
*
* @return void
*/
public function setup()
{
InternalIndexInitializer::initialize($this);
}
/**
* Create new document for this index.
*
* @return DocumentInterface
*/
public function createDocument()
{
return $this->documentGenerator->generate();
}
}
@@ -0,0 +1,52 @@
<?php
namespace Common\Util\Index;
use IndexBundle\Index\Source\SourceIndexInterface;
use IndexBundle\Model\Generator\SourceDocumentGenerator;
use IndexBundle\Model\DocumentInterface;
use IndexBundle\Util\Initializer\SourceIndexInitializer;
/**
* Class InternalSourceConnection
* @package Common\Util\Index
*/
class InternalSourceConnection extends AbstractTestIndexConnection implements SourceIndexInterface
{
/**
* @var SourceDocumentGenerator
*/
private $documentGenerator;
/**
* ExternalIndexConnection constructor.
*
* @param SourceIndexInterface $index A SourceIndexInterface interface.
*/
public function __construct(SourceIndexInterface $index)
{
parent::__construct($index);
$this->documentGenerator = new SourceDocumentGenerator();
}
/**
* Setup internal index.
*
* @return void
*/
public function setup()
{
SourceIndexInitializer::initialize($this);
}
/**
* Create new document for this index.
*
* @return DocumentInterface
*/
public function createDocument()
{
return $this->documentGenerator->generate();
}
}
@@ -0,0 +1,64 @@
<?php
namespace Common\Util\Index;
use IndexBundle\Index\External\ExternalIndexInterface;
use IndexBundle\Index\IndexInterface;
use IndexBundle\Index\Internal\InternalIndexInterface;
use IndexBundle\Index\Source\SourceIndexInterface;
use IndexBundle\Model\DocumentInterface;
/**
* Interface TestIndexConnectionInterface
*
* @package Common\Util\Index
*/
interface TestIndexConnectionInterface extends InternalIndexInterface
{
/**
* Setup index with mappings.
*
* @return void
*/
public function setup();
/**
* Create new document for this index.
*
* @return DocumentInterface
*/
public function createDocument();
/**
* Create new index.
*
* @param array $mapping Index mapping.
* @param array $settings Index settings.
*
* @return void
*/
public function createIndex(array $mapping, array $settings = []);
/**
* Index given document or array of documents.
*
* @param DocumentInterface|DocumentInterface[] $data DocumentInterface instance
* or array of instances.
*
* @return void
*/
public function index($data);
/**
* Purge index.
*
* @return void
*/
public function purge();
/**
* @return IndexInterface|InternalIndexInterface|ExternalIndexInterface|SourceIndexInterface
*/
public function getIndex();
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace Common\Util\Matcher;
/**
* Class AppMatcher
* Facade for matching process.
*
* @package Common\Util\Matcher
*/
class AppMatcher
{
/**
* Array of entities metadata.
* Make static because for some reason ExpanderInitializer from
* coduo/php-Matcher mark as final and haven't interface, so we can't override
* or decorate it and pass this entities into it. Therefore we can't pass
* this map into concrete expander ... :-(
*
* @var array
*/
private static $entities;
/**
* @param array $entities Array of entities pattern.
*
* @return void
*/
public static function registerEntities(array $entities = [])
{
self::$entities = $entities;
}
/**
* @param string $entityName Entity name.
*
* @return \Common\Util\Metadata\EntityMetadata
*/
public static function getEntityMetadata($entityName)
{
return self::$entities[$entityName];
}
/**
* @param string $value Checked value.
* @param string $pattern Pattern.
* @param null|string $error Error.
*
* @return boolean
*/
public static function match($value, $pattern, &$error = null)
{
$factory = new MatcherFactory();
$matcher = $factory->createMatcher();
if (! $matcher->match($value, $pattern)) {
$error = $matcher->getError();
return false;
}
return true;
}
}
@@ -0,0 +1,60 @@
<?php
namespace Common\Util\Matcher\Expander;
use Coduo\PHPMatcher\Matcher\Pattern\PatternExpander;
/**
* Class AbstractChainExpander
* @package Common\Util\Matcher\Expander
*/
abstract class AbstractChainExpander extends AbstractExpander
{
/**
* @var PatternExpander[]
*/
protected $expanders = [];
/**
* @param PatternExpander $expander A PatternExpander instance.
* @param PatternExpander $expander,... A PatternExpander's instances.
*/
public function __construct(PatternExpander $expander)
{
$this->expanders[] = $expander;
if (func_num_args() > 1) {
$arguments = func_get_args();
$length = count($arguments);
for ($i = 1; $i < $length; ++$i) {
if (!$arguments[$i] instanceof PatternExpander) {
throw new \InvalidArgumentException('Has invalid expander.');
}
$this->expanders[] = $arguments[$i];
}
}
}
/**
* @param mixed $value Value to match.
*
* @return boolean
*/
public function match($value)
{
foreach ($this->expanders as $expander) {
if (! $expander->match($value)) {
$className = get_class($expander);
$className = substr($className, strrpos($className, '\\'));
$this->error = "Expander {$className} don't matches value: ".
$expander->getError();
return false;
}
}
return true;
}
}
@@ -0,0 +1,26 @@
<?php
namespace Common\Util\Matcher\Expander;
use Coduo\PHPMatcher\Matcher\Pattern\PatternExpander;
/**
* Class AbstractExpander
* @package Common\Util\Matcher\Expander
*/
abstract class AbstractExpander implements PatternExpander
{
/**
* @var string|null
*/
protected $error;
/**
* @return string|null
*/
public function getError()
{
return $this->error;
}
}
@@ -0,0 +1,78 @@
<?php
namespace Common\Util\Matcher\Expander;
use Common\Util\Converter\DateConverter;
/**
* Class BetweenExpander
* Check that value is between specified bounds.
* Except integers, float and datetime values.
*
* Example:
* - integer.between(10, 20)
* - date.between('2017-10-01', '2017-11-01')
* - .field('date', between('2017-10-01', '2017-11-01'))
*
* @package Common\Util\Matcher\Expander
*/
class BetweenExpander extends AbstractExpander
{
/**
* @var integer|float|string
*/
private $start;
/**
* @var integer|float|string
*/
private $end;
/**
* @param integer|float|string $start Start bound.
* @param integer|float|string $end End bound.
*/
public function __construct($start, $end)
{
$this->start = $start;
$this->end = $end;
}
/**
* @param mixed $value Value to match.
*
* @return boolean
*/
public function match($value)
{
if (! is_numeric($value) && ! is_int($value) && ! is_float($value)
&& ! is_string($value)) {
$this->error = 'Can match only integers, float and datetime values';
return false;
}
$start = $this->start;
$end = $this->end;
if (DateConverter::can($value)) {
// For string which represent date try to convert it into \DateTime
// instances.
try {
$value = DateConverter::convert($value);
$start = DateConverter::convert($start);
$end = DateConverter::convert($end);
$value->setTimezone($start->getTimezone());
} catch (\Exception $e) {
$this->error = $e->getMessage();
return false;
}
} else {
// For scalar types convert all values to the same type.
$type = gettype($start);
settype($value, $type);
}
return ($value >= $start) && ($value <= $end);
}
}
@@ -0,0 +1,69 @@
<?php
namespace Common\Util\Matcher\Expander;
use Common\Util\Matcher\AppMatcher;
/**
* Class EntityExpander
* Check that expanded object is serialized application entity.
*
* Example: object.entity('AppBundle:User', 'user, post')
*
* @package Util\Matcher\Expander
*/
class EntityExpander extends AbstractExpander
{
/**
* @var string
*/
private $entityName;
/**
* @var string[]
*/
private $serializationGroups;
/**
* @param string $entityName Entity name like
* 'BundleName:EntityName'.
* @param string|array $serializationGroups Serialization groups in string
* format delimited by ','.
*/
public function __construct($entityName, $serializationGroups = [])
{
$this->entityName = $entityName;
// Split serialization groups string into array.
$serializationGroups = explode(',', $serializationGroups);
// Trim values and remove empty.
$serializationGroups = array_filter(
array_map('trim', $serializationGroups)
);
$this->serializationGroups = $serializationGroups;
}
/**
* @param mixed $value Value to match.
*
* @return boolean
*/
public function match($value)
{
// Get entity metadata for specified entity.
$metadata = AppMatcher::getEntityMetadata($this->entityName);
// Get patter fot specified entity with given serialization group.
$pattern = $metadata->getPattern($this->serializationGroups);
if ($pattern && ! AppMatcher::match($value, $pattern, $this->error)) {
$this->error =
"Invalid entity {$this->entityName}: {$this->error}";
return false;
}
return true;
}
}
@@ -0,0 +1,43 @@
<?php
namespace Common\Util\Matcher\Expander;
use Coduo\ToString\StringConverter;
/**
* Class EveryExpander
* Check that every array elements matches specified expander.
*
* Example:
* - array.every(field('property', value))
* - array.every(entity('AppBundle:User', 'user, post'))
*
* @package Common\Util\Matcher\Expander
*/
class EveryExpander extends AbstractChainExpander
{
/**
* @param mixed $value Value to match.
*
* @return boolean
*/
public function match($value)
{
if (! is_array($value)) {
$this->error = 'Every expander require "array", got '.
new StringConverter($value) .'.';
return false;
}
foreach ($value as $row) {
if (! parent::match($row)) {
$this->error = 'Checked value '. new StringConverter($row)
.' is invalid: '. $this->error;
return false;
}
}
return true;
}
}
@@ -0,0 +1,88 @@
<?php
namespace Common\Util\Matcher\Expander;
use Coduo\PHPMatcher\Matcher\Pattern\PatternExpander;
/**
* Class FieldExpander
* Check that expanded object or array has specific field which matched
* given matcher expander.
*
* Example:
* - .field('username', contains('admin'))
* - .field('user', entity('AppBundle:User', 'user'), field('id', 1))
*
* @package Common\Util\Matcher\Expander
*/
class FieldExpander extends AbstractExpander
{
/**
* @var string
*/
private $fieldName;
/**
* @var mixed|PatternExpander[]
*/
private $expander;
/**
* @param string $fieldName Field name.
* @param mixed|PatternExpander $expander A PatternExpander instance.
* @param PatternExpander $expander,... A PatternExpander's instances.
*/
public function __construct($fieldName, $expander)
{
$this->fieldName = $fieldName;
$this->expander = $expander;
if ($expander instanceof PatternExpander) {
$expander = func_get_args();
$length = count($expander);
// Process all except first argument which contains field name.
$this->expander = [];
for ($i = 1; $i < $length; ++$i) {
if (!$expander[$i] instanceof PatternExpander) {
throw new \InvalidArgumentException('Has invalid expander.');
}
$this->expander[] = $expander[$i];
}
}
}
/**
* @param mixed $value Value to match.
*
* @return boolean
*/
public function match($value)
{
if (! is_array($value) && !isset($value[$this->fieldName])) {
return false;
}
if (is_array($this->expander)) {
// Match all expanders.
foreach ($this->expander as $expander) {
if (! $expander->match($value[$this->fieldName])) {
$this->error = "Field {$this->fieldName}: expander don't matches value. ".
$expander->getError();
return false;
}
}
// All expanders successfully matches.
return true;
}
if ($value[$this->fieldName] !== $this->expander) {
$this->error = "Field {$this->fieldName}: don't equal to {$this->expander}";
return false;
}
return true;
}
}
@@ -0,0 +1,78 @@
<?php
namespace Common\Util\Matcher\Expander;
use Common\Util\Converter\DateConverter;
/**
* Class GteExpander
* Check that value is greater or equal to another value.
* Except integers, float and datetime values.
*
* Example:
* - integer.gte(10)
* - date.gte('2017-10-01')
* - .field('date', gte('2017-10-01'))
*
* @package Common\Util\Matcher\Expander
*/
class GteExpander extends AbstractExpander
{
/**
* @var integer|string|\DateTimeInterface|float
*/
private $value;
/**
* @param integer|string|\DateTimeInterface|float $value Expected value.
*/
public function __construct($value)
{
$this->value = $value;
}
/**
* @param mixed $value Value to match.
*
* @return boolean
*/
public function match($value)
{
if (! is_numeric($value) && ! is_int($value) && ! is_float($value)
&& ! is_string($value)) {
$this->error = 'Can match only integers, float and datetime values';
return false;
}
$bound = $this->value;
if (DateConverter::can($value)) {
// For string which represent date try to convert it into \DateTime
// instances.
try {
$value = DateConverter::convert($value);
$bound = DateConverter::convert($bound);
$bound->setTimezone($value->getTimezone());
} catch (\Exception $e) {
$this->error = $e->getMessage();
return false;
}
} else {
// For scalar types convert all values to the same type.
$type = gettype($bound);
settype($value, $type);
}
if (! ($matched = $value >= $bound)) {
if ($value instanceof \DateTime) {
$value = $value->format('c');
$bound = $bound->format('c');
}
$this->error = "Checked value {$value} less than {$bound}.";
}
return $matched;
}
}
@@ -0,0 +1,52 @@
<?php
namespace Common\Util\Matcher\Expander;
use Coduo\PHPMatcher\Matcher\Pattern\PatternExpander;
/**
* Class LengthExpander
* Check that expanded array contains specified number of elements.
*
* Example: array.length(2)
*
* @package Common\Util\Matcher\Expander
*/
class LengthExpander extends AbstractExpander
{
/**
* @var integer
*/
private $count;
/**
* @param PatternExpander|integer $count Expected number of elements or
* appropriate pattern expander.
*/
public function __construct($count)
{
if (! $count instanceof PatternExpander) {
$count = (int) $count;
}
$this->count = $count;
}
/**
* @param mixed $value Value to match.
*
* @return boolean
*/
public function match($value)
{
if (! is_array($value)) {
return false;
}
if ($this->count instanceof PatternExpander) {
return $this->count->match(count($value));
}
return count($value) === $this->count;
}
}
@@ -0,0 +1,44 @@
<?php
namespace Common\Util\Matcher\Expander;
use Coduo\PHPMatcher\Matcher\Pattern\PatternExpander;
/**
* Class FieldExpander
* Check that inner expander not match.
*
* Example: .not(contains('some'))
*
* @package Common\Util\Matcher\Expander
*/
class NotExpander extends AbstractExpander
{
/**
* @var mixed
*/
private $expander;
/**
* @param PatternExpander $expander A PatternExpander instance.
*/
public function __construct(PatternExpander $expander)
{
$this->expander = $expander;
}
/**
* @param mixed $value Value to match.
*
* @return boolean
*/
public function match($value)
{
if ($this->expander->match($value)) {
$this->error = 'Expander match this value.';
return false;
}
return true;
}
}
@@ -0,0 +1,40 @@
<?php
namespace Common\Util\Matcher\Expander;
use Coduo\ToString\StringConverter;
/**
* Class OneExpander
* Check that only one array element matches specified expander.
*
* Example:
* - array.one(field('property', value))
* - array.one(entity('AppBundle:User'), field('id', 1))
*
* @package Common\Util\Matcher\Expander
*/
class OneExpander extends AbstractChainExpander
{
/**
* @param mixed $value Value to match.
*
* @return boolean
*/
public function match($value)
{
if (! is_array($value)) {
$this->error = 'One expander require "array", got '.
new StringConverter($value) .'.';
return false;
}
foreach ($value as $row) {
if (parent::match($row)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,43 @@
<?php
namespace Common\Util\Matcher\Expander;
use Coduo\ToString\StringConverter;
/**
* Class SomeExpander
* Check that some array elements matches specified expander.
*
* Example:
* - array.some(field('property', value))
* - array.some(entity('AppBundle:User'), field('id', 1))
*
* @package Common\Util\Matcher\Expander
*/
class SomeExpander extends AbstractChainExpander
{
/**
* @param mixed $value Value to match.
*
* @return boolean
*/
public function match($value)
{
if (! is_array($value)) {
$this->error = 'Some expander require "array", got '.
new StringConverter($value) .'.';
return false;
}
foreach ($value as $row) {
if (parent::match($row)) {
return true;
}
}
$this->error = 'No element does not match specified expanders';
return false;
}
}
@@ -0,0 +1,46 @@
<?php
namespace Common\Util\Matcher\Expander;
/**
* Class TypeExpander
* Check that value has specified type.
*
* Example:
* - wildcart.oneOf(isEmpty(), type('double'))
* - wildcart.type('string')
*
* @package Common\Util\Matcher\Expander
*/
class TypeExpander extends AbstractExpander
{
/**
* @var integer
*/
private $type;
/**
* @param string $type Expected value type.
*/
public function __construct($type)
{
$this->type = $type;
}
/**
* @param mixed $value Value to match.
*
* @return boolean
*/
public function match($value)
{
$valueType = gettype($value);
if ($valueType === 'float') {
$valueType = 'double';
}
return $valueType === $this->type;
}
}
@@ -0,0 +1,94 @@
<?php
namespace Common\Util\Matcher\Matcher;
use Coduo\PHPMatcher\Matcher\Matcher;
use Coduo\PHPMatcher\Matcher\Pattern\Assert\Json;
use Coduo\PHPMatcher\Matcher\ValueMatcher;
/**
* Class JsonMatcher
* @package Common\Util\Matcher\Matcher
*/
class JsonMatcher extends Matcher
{
/**
* @var
*/
private $matcher;
/**
* @param ValueMatcher $matcher
*/
public function __construct(ValueMatcher $matcher)
{
$this->matcher = $matcher;
}
/**
* Checks if matcher can match the pattern
*
* @param mixed $pattern Pattern.
*
* @return boolean
*/
public function canMatch($pattern)
{
return Json::isValidPattern($pattern);
}
/**
* Matches value against the pattern
*
* @param mixed $value Checked value.
* @param mixed $pattern Pattern.
*
* @return boolean
*/
public function match($value, $pattern)
{
if (parent::match($value, $pattern)) {
return true;
}
if (!Json::isValid($value)) {
$this->error = sprintf("Invalid given JSON of value. %s", $this->getErrorMessage());
return false;
}
if (!Json::isValidPattern($pattern) ) {
$this->error = sprintf("Invalid given JSON of pattern. %s", $this->getErrorMessage());
return false;
}
$transformedPattern = Json::transformPattern($pattern);
$match = $this->matcher->match(json_decode($value, true), json_decode($transformedPattern, true));
if (!$match) {
$this->error = $this->matcher->getError();
return false;
}
return true;
}
/**
* @return string
*/
private function getErrorMessage()
{
switch(json_last_error()) {
case JSON_ERROR_DEPTH:
return 'Maximum stack depth exceeded';
case JSON_ERROR_STATE_MISMATCH:
return 'Underflow or the modes mismatch';
case JSON_ERROR_CTRL_CHAR:
return 'Unexpected control character found';
case JSON_ERROR_SYNTAX:
return 'Syntax error, malformed JSON';
case JSON_ERROR_UTF8:
return 'Malformed UTF-8 characters, possibly incorrectly encoded';
default:
return 'Unknown error';
}
}
}
@@ -0,0 +1,88 @@
<?php
namespace Common\Util\Matcher\Matcher;
use Coduo\PHPMatcher\Matcher\Matcher;
use Coduo\PHPMatcher\Parser;
use Seld\JsonLint\JsonParser;
/**
* Class ObjectMatcher
* Add 'object' pattern.
* Used by object expander's which makes all useful tests.
*
* @package Common\Util\Matcher\Matcher
*/
class ObjectMatcher extends Matcher
{
/**
* @var Parser
*/
private $parser;
/**
* @param Parser $parser A Parser instance.
*/
public function __construct(Parser $parser)
{
$this->parser = $parser;
}
/**
* Checks if matcher can match the pattern
*
* @param mixed $pattern Pattern.
*
* @return boolean
*/
public function canMatch($pattern)
{
if (! is_string($pattern)) {
return false;
}
return $this->parser->hasValidSyntax($pattern)
&& $this->parser->parse($pattern)->is('object');
}
/**
* Matches value against the pattern
*
* @param mixed $value Checked value.
* @param mixed $pattern Pattern.
*
* @return boolean
*/
public function match($value, $pattern)
{
if (parent::match($value, $pattern)) {
return true;
}
// Add ability to match serialized json.
$lint = new JsonParser();
if (is_string($value) && ($lint->lint($value) === null)) {
$value = json_decode($value, true);
}
if (! is_array($value)) {
return false;
}
// Check that given value is assoc array.
if (array_keys($value) === range(0, count($value) - 1)) {
return false;
}
$typePattern = $this->parser->parse($pattern);
// Match all expanders.
if (!$typePattern->matchExpanders($value)) {
$this->error = $typePattern->getError();
return false;
}
return true;
}
}
@@ -0,0 +1,66 @@
<?php
namespace Common\Util\Matcher\Matcher;
use Coduo\PHPMatcher\Matcher\Matcher;
use Coduo\PHPMatcher\Parser;
/**
* Class WildcardMatcher
* Replace default coduo/php-matcher WildcardMatcher in order to process
* expanders.
*
* @package Common\Util\Matcher\Matcher
*/
class WildcardMatcher extends Matcher
{
/**
* @var Parser
*/
private $parser;
/**
* @param Parser $parser A Parser instance.
*/
public function __construct(Parser $parser)
{
$this->parser = $parser;
}
const MATCH_PATTERN = "@wildcard@";
/**
* Checks if matcher can match the pattern
*
* @param mixed $pattern Pattern.
*
* @return boolean
*/
public function canMatch($pattern)
{
return is_string($pattern)
&& strpos($pattern, self::MATCH_PATTERN) !== false;
}
/**
* Matches value against the pattern
*
* @param mixed $value Checked value.
* @param mixed $pattern Pattern.
*
* @return boolean
*/
public function match($value, $pattern)
{
$typePattern = $this->parser->parse($pattern);
// Match all expanders.
if (!$typePattern->matchExpanders($value)) {
$this->error = $typePattern->getError();
return false;
}
return true;
}
}
@@ -0,0 +1,103 @@
<?php
namespace Common\Util\Matcher;
use Coduo\PHPMatcher\Factory\SimpleFactory;
use Coduo\PHPMatcher\Lexer;
use Coduo\PHPMatcher\Matcher\ChainMatcher;
use Coduo\PHPMatcher\Parser;
use Coduo\PHPMatcher\Matcher;
use Common\Util\Matcher\Expander as AppExpanders;
use Common\Util\Matcher\Matcher as AppMatchers;
/**
* Class MatcherFactory
* @package Common\Util\Matcher
*/
class MatcherFactory extends SimpleFactory
{
/**
* @var Parser
*/
private static $parser;
/**
* @var array
*/
private static $additionalExpanders = [
'field' => AppExpanders\FieldExpander::class,
'some' => AppExpanders\SomeExpander::class,
'every' => AppExpanders\EveryExpander::class,
'length' => AppExpanders\LengthExpander::class,
'one' => AppExpanders\OneExpander::class,
'type' => AppExpanders\TypeExpander::class,
'entity' => AppExpanders\EntityExpander::class,
'not' => AppExpanders\NotExpander::class,
'gte' => AppExpanders\GteExpander::class,
'between' => AppExpanders\BetweenExpander::class,
];
/**
* @return ChainMatcher
*/
protected function buildScalarMatchers()
{
$parser = $this->buildParser();
return new Matcher\ChainMatcher([
// Default matchers.
new Matcher\CallbackMatcher(),
new Matcher\ExpressionMatcher(),
new Matcher\NullMatcher(),
new Matcher\StringMatcher($parser),
new Matcher\IntegerMatcher($parser),
new Matcher\BooleanMatcher(),
new Matcher\DoubleMatcher($parser),
new Matcher\NumberMatcher(),
new Matcher\ScalarMatcher(),
// Custom matchers.
new AppMatchers\ObjectMatcher($parser),
new AppMatchers\WildcardMatcher($parser),
]);
}
/**
* @return Parser
*/
protected function buildParser()
{
if (!self::$parser) {
// Register all expanders.
$expanderInitializer = new Parser\ExpanderInitializer();
foreach (self::$additionalExpanders as $name => $class) {
$expanderInitializer->setExpanderDefinition($name, $class);
}
self::$parser = new Parser(new Lexer(), $expanderInitializer);
}
return self::$parser;
}
/**
* @return \Coduo\PHPMatcher\Matcher\ChainMatcher
*/
protected function buildMatchers()
{
$scalarMatchers = $this->buildScalarMatchers();
$orMatcher = $this->buildOrMatcher();
$chainMatcher = new Matcher\ChainMatcher([
$scalarMatchers,
$orMatcher,
new AppMatchers\JsonMatcher($orMatcher),
new Matcher\XmlMatcher($orMatcher),
new Matcher\TextMatcher($scalarMatchers, $this->buildParser()),
]);
return $chainMatcher;
}
}
@@ -0,0 +1,145 @@
<?php
namespace Common\Util\Metadata;
use ApiBundle\Serializer\Metadata\Metadata;
use ApiBundle\Serializer\Metadata\PropertyMetadata;
use AppBundle\Entity\EntityInterface;
/**
* Class EntityMetadata
* Contains all entity metadata for matcher.
*
* @package Common\Util\Metadata
*/
class EntityMetadata
{
/**
* @var Metadata
*/
private $metadata;
/**
* @var array[]
*/
private $cache = [];
/**
* @param Metadata $metadata A Metadata instance.
*/
public function __construct(Metadata $metadata)
{
$this->metadata = $metadata;
}
/**
* Get pattern for specified groups.
*
* @param array $groups Serialization groups.
*
* @return array
*/
public function getPattern(array $groups)
{
sort($groups);
$key = serialize($groups);
if (! isset($this->cache[$key])) {
$properties = $this->metadata->getProperties($groups);
$this->cache[$key] = [];
if ($this->metadata->implementsInterface(EntityInterface::class)) {
$this->cache[$key]['type'] = '@string@';
}
foreach ($properties as $property) {
$this->cache[$key][$property->getName()] =
$this->generateProperty($property, $groups);
}
}
return $this->cache[$key];
}
/**
* Convert from entity metadata type to PHPMatcher type.
*
* @param PropertyMetadata $property A PropertyMetadata instance.
* @param array $groups A serialization groups.
*
* @return string
*/
private function generateProperty(PropertyMetadata $property, array $groups)
{
$type = $property->getType();
switch ($type) {
// Associated object - another entity.
// Use entity expander with same serialization groups.
case PropertyMetadata::TYPE_ENTITY:
$type = \app\c\entityFqcnToShort($property->getActualType());
$expander = "entity('{$type}', '". implode(',', $groups) ."')";
$type = "@object@.{$expander}";
if ($property->isNullable()) {
$type = "@wildcard@.oneOf(isEmpty(), {$expander})";
}
break;
// Enum type.
case PropertyMetadata::TYPE_ENUM:
$type = '@string@';
break;
// Collection of associated entities.
case PropertyMetadata::TYPE_COLLECTION:
$type = \app\c\entityFqcnToShort($property->getActualType());
$reflection = new \ReflectionClass($property->getActualType());
if ($reflection->isAbstract()) {
$type = '@array@';
} else {
$expander = "entity('{$type}', '" . implode(',', $groups) . "')";
$type = "@array@.every({$expander})";
if ($property->isNullable()) {
$type = "@wildcard@.oneOf(isEmpty(), every({$expander}))";
}
}
break;
// For double type also use integer type checker.
case PropertyMetadata::TYPE_DOUBLE:
$type = "@wildcard@.oneOf(type('integer'), type('double'))";
if ($property->isNullable()) {
$type = "@wildcard@.oneOf(isEmpty(), type('integer'), type('double'))";
}
break;
// For DateTime instances use 'string' matcher with 'isDateTime'
// expander.
case PropertyMetadata::TYPE_DATE:
$type = '@string@.isDateTime()';
if ($property->isNullable()) {
$type = '@wildcard@.oneOf(isEmpty(), isDateTime())';
}
break;
// Process inline objects.
case PropertyMetadata::TYPE_GROUP:
$type = '@object@';
if ($property->isNullable()) {
$type = '@wildcard@';
}
break;
// Other types like string, integer and etc.
default:
if ($property->isNullable()) {
$type = "@wildcard@.oneOf(isEmpty(), type('$type'))";
} else {
$type = '@'. $type .'@';
}
}
return $type;
}
}
@@ -0,0 +1,88 @@
<?php
namespace Common\Util\Processor;
use Common\Util\Processor\ExpressionLanguage\TestExpressionLanguage;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
/**
* Class DataProcessor
* Process data before sending to server.
*
* @package Common\Util\Processor
*/
class DataProcessor
{
/**
* @var ExpressionLanguage
*/
private $language;
/**
* DataProcessor constructor.
*
* @param ContainerInterface $container A ContainerInterface instance.
*/
public function __construct(ContainerInterface $container)
{
$this->language = new TestExpressionLanguage($container);
$this->registerFunctions();
}
/**
* @param mixed $data Process data send to server and replace patterns.
*
* @return mixed
*/
public function process($data)
{
if (is_array($data)) {
// Recursively process arrays.
return array_map(function ($data) {
return $this->process($data);
}, $data);
} elseif (is_string($data)) {
// Process string data.
$replacer = function ($param) {
// Sanitize params.
$param = str_replace('\\"', '\'', trim(current($param), '#'));
return $this->language->evaluate($param);
};
return preg_replace_callback("/#.+?#/", $replacer, $data);
}
// Not change other variable types.
return $data;
}
/**
* @param mixed $arguments Arguments specified by expression language.
* @param mixed $time Pass to DateTime constructor.
*
* @return \DateTime
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function createDate($arguments, $time = 'now')
{
return new \DateTime($time);
}
/**
* Register custom expression language functions.
*
* @return void
*/
private function registerFunctions()
{
$dummy = function () {
// do nothing.
};
$this->language->register('date', $dummy, [ $this, 'createDate' ]);
}
}
@@ -0,0 +1,75 @@
<?php
namespace Common\Util\Processor\ExpressionLanguage\Provider;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
/**
* Class EntityGetterProvider
* Register function like 'getUser' or 'getStoredQuery' for fetching single
* entity from database.
*
* @package Common\Util\Processor\ExpressionLanguage\Provider
*
*/
class EntityGetterProvider implements ExpressionFunctionProviderInterface
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* EntityGetterProvider constructor.
*
* @param EntityManagerInterface $em A EntityManagerInterface instance.
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* @return ExpressionFunction[] An array of Function instances.
*/
public function getFunctions()
{
/**
* Dummy compiler.
* We use this expression function only in runtime and not compile its.
*/
$compiler = function () {
};
$functions = [];
/** @var ClassMetadataInfo[] $metadataList */
$metadataList = $this->em->getMetadataFactory()->getAllMetadata();
foreach ($metadataList as $metadata) {
$name = $metadata->getName();
$shortName = substr($name, strrpos($name, '\\') + 1);
$fnName = 'get'. $shortName;
if (strpos($name, 'Entity\\'. $shortName) === false) {
// Process only entities inside 'Entity' directory.
continue;
}
/**
* @param mixed $arguments Arguments specified by expression language
* @param array $criteria Search criteria.
*
* @return null|object Found entity or null.
*/
$evaluator = function ($arguments, array $criteria) use ($name) {
return $this->em->getRepository($name)->findOneBy($criteria);
};
$functions[] = new ExpressionFunction($fnName, $compiler, $evaluator);
}
return $functions;
}
}
@@ -0,0 +1,41 @@
<?php
namespace Common\Util\Processor\ExpressionLanguage;
use Common\Util\Processor\ExpressionLanguage\Provider\EntityGetterProvider;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
/**
* Class TestExpressionLanguage
* @package Common\Util\Processor\ExpressionLanguage
*/
class TestExpressionLanguage extends ExpressionLanguage
{
/**
* TestExpressionLanguage constructor.
*
* @param ContainerInterface $container A ContainerInterface instance.
*/
public function __construct(ContainerInterface $container)
{
/** @var Registry $doctrine */
$doctrine = $container->get('doctrine');
$dummy = function () {
// Dummy function for compiler argument of ExpressionFunction.
};
parent::__construct(null, [
new EntityGetterProvider($doctrine->getManager()),
]);
// Add 'now' function which return current date.
$this->addFunction(new ExpressionFunction('now', $dummy, function () {
return date_create();
}));
}
}