This commit is contained in:
Tim Nagel 2014-04-25 21:31:04 +10:00
parent 4c961a757d
commit 15d3f1e4f8
23 changed files with 912 additions and 13 deletions

View file

@ -0,0 +1,22 @@
<?php
namespace FOS\ElasticaBundle\Doctrine;
use Doctrine\Common\Persistence\ManagerRegistry;
use FOS\ElasticaBundle\Type\LookupInterface;
abstract class AbstractLookup implements LookupInterface
{
/**
* @var \Doctrine\Common\Persistence\ManagerRegistry
*/
protected $registry;
/**
* @param ManagerRegistry $registry
*/
public function __construct(ManagerRegistry $registry)
{
$this->registry = $registry;
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace FOS\ElasticaBundle\Doctrine\MongoDB;
use FOS\ElasticaBundle\Doctrine\AbstractLookup;
use FOS\ElasticaBundle\Type\TypeConfigurationInterface;
class Lookup extends AbstractLookup
{
/**
* Returns the lookup key.
*
* @return string
*/
public function getKey()
{
return 'mongodb';
}
/**
* Look up objects of a specific type with ids as supplied.
*
* @param TypeConfigurationInterface $configuration
* @param array $ids
* @return array
*/
public function lookup(TypeConfigurationInterface $configuration, array $ids)
{
$qb = $this->createQueryBuilder($configuration);
$qb->hydrate($configuration->isHydrate());
$qb->field($configuration->getIdentifierProperty())
->in($ids);
return $qb->getQuery()->execute()->toArray();
}
/**
* @param TypeConfigurationInterface $configuration
* @return \Doctrine\ODM\MongoDB\Query\Builder
*/
private function createQueryBuilder(TypeConfigurationInterface $configuration)
{
$method = $configuration->getRepositoryMethod();
$manager = $this->registry->getManagerForClass($configuration->getModelClass());
return $manager->{$method}($configuration->getModelClass());
}
}

58
Doctrine/ORM/Lookup.php Normal file
View file

@ -0,0 +1,58 @@
<?php
namespace FOS\ElasticaBundle\Doctrine\ORM;
use Doctrine\ORM\Query;
use FOS\ElasticaBundle\Doctrine\AbstractLookup;
use FOS\ElasticaBundle\Type\TypeConfigurationInterface;
class Lookup extends AbstractLookup
{
const ENTITY_ALIAS = 'o';
/**
* Returns the lookup key.
*
* @return string
*/
public function getKey()
{
return 'orm';
}
/**
* Look up objects of a specific type with ids as supplied.
*
* @param TypeConfigurationInterface $configuration
* @param array $ids
* @return array
*/
public function lookup(TypeConfigurationInterface $configuration, array $ids)
{
$hydrationMode = $configuration->isHydrate() ?
Query::HYDRATE_OBJECT :
Query::HYDRATE_ARRAY;
$qb = $this->createQueryBuilder($configuration);
$qb->andWhere($qb->expr()->in(
sprintf('%s.%s', static::ENTITY_ALIAS, $configuration->getIdentifierProperty()),
':identifiers'
));
$qb->setParameter('identifiers', $ids);
return $qb->getQuery()->execute(array(), $hydrationMode);
}
/**
* @param TypeConfigurationInterface $configuration
* @return \Doctrine\ORM\QueryBuilder
*/
private function createQueryBuilder(TypeConfigurationInterface $configuration)
{
$repository = $this->registry->getRepository($configuration->getModelClass());
$method = $configuration->getRepositoryMethod();
return $repository->{$method}(static::ENTITY_ALIAS);
}
}

View file

@ -5,6 +5,7 @@ namespace FOS\ElasticaBundle\Elastica;
use Elastica\Client as Client;
use Elastica\Request;
use FOS\ElasticaBundle\Logger\ElasticaLogger;
use FOS\ElasticaBundle\Transformer\CombinedResultTransformer;
/**
* Extends the default Elastica client to provide logging for errors that occur
@ -14,6 +15,38 @@ use FOS\ElasticaBundle\Logger\ElasticaLogger;
*/
class LoggingClient extends Client
{
/**
* @var CombinedResultTransformer
*/
private $resultTransformer;
public function __construct(array $config = array(), $callback = null, CombinedResultTransformer $resultTransformer)
{
parent::__construct($config, $callback);
$this->resultTransformer = $resultTransformer;
}
/**
* Overridden Elastica method to return TransformingIndex instances instead of the
* default Index instances.
*
* @param string $name
* @return TransformingIndex
*/
public function getIndex($name)
{
return new TransformingIndex($this, $name, $this->resultTransformer);
}
/**
* @return CombinedResultTransformer
*/
public function getResultTransformer()
{
return $this->resultTransformer;
}
/**
* {@inheritdoc}
*/
@ -39,9 +72,4 @@ class LoggingClient extends Client
return $response;
}
public function getIndex($name)
{
return new DynamicIndex($this, $name);
}
}

View file

@ -1,8 +1,11 @@
<?php
namespace FOS\ElasticaBundle\Index;
namespace FOS\ElasticaBundle\Elastica;
use Elastica\Client;
use Elastica\Exception\InvalidException;
use Elastica\Index;
use FOS\ElasticaBundle\Transformer\CombinedResultTransformer;
/**
* Overridden Elastica Index class that provides dynamic index name changes
@ -14,20 +17,32 @@ use Elastica\Index;
class TransformingIndex extends Index
{
/**
* Indexes a
* @param string $query
* @param int|array $options
* @return \Elastica\Search
* Creates a TransformingSearch instance instead of the default Elastica Search
*
* @param string $query
* @param int|array $options
* @return TransformingSearch
*/
public function createSearch($query = '', $options = null)
{
$search = new Search($this->getClient());
$search = new TransformingSearch($this->getClient());
$search->addIndex($this);
$search->setOptionsAndQuery($options, $query);
return $search;
}
/**
* Returns a type object for the current index with the given name
*
* @param string $type Type name
* @return TransformingType Type object
*/
public function getType($type)
{
return new TransformingType($this, $type);
}
/**
* Reassign index name
*
@ -35,8 +50,6 @@ class TransformingIndex extends Index
* since it's used for a very specific case and normally should not be used
*
* @param string $name Index name
*
* @return void
*/
public function overrideName($name)
{

View file

@ -0,0 +1,51 @@
<?php
namespace FOS\ElasticaBundle\Elastica;
use Elastica\Result;
class TransformingResult extends Result
{
/**
* The transformed hit.
*
* @var mixed
*/
private $transformed;
/**
* @var TransformingResultSet
*/
private $resultSet;
public function __construct(array $hit, TransformingResultSet $resultSet)
{
parent::__construct($hit);
$this->resultSet = $resultSet;
}
/**
* Returns the transformed result of the hit.
*
* @return mixed
*/
public function getTransformed()
{
if (null === $this->transformed) {
$this->resultSet->transform();
}
return $this->transformed;
}
/**
* An internal method used to set the transformed result on the Result.
*
* @internal
*/
public function setTransformed($transformed)
{
$this->transformed = $transformed;
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace FOS\ElasticaBundle\Elastica;
use Elastica\Query;
use Elastica\Response;
use Elastica\ResultSet;
use FOS\ElasticaBundle\Transformer\CombinedResultTransformer;
use FOS\ElasticaBundle\Transformer\TransformerFactoryInterface;
class TransformingResultSet extends ResultSet
{
/**
* @var \FOS\ElasticaBundle\Transformer\CombinedResultTransformer
*/
private $resultTransformer;
/**
* If a transformation has already been performed on this ResultSet or not.
*
* @var bool
*/
private $transformed = false;
public function __construct(Response $response, Query $query, CombinedResultTransformer $resultTransformer)
{
parent::__construct($response, $query);
$this->resultTransformer = $resultTransformer;
}
/**
* Overridden default method to set our TransformingResult objects.
*
* @param \Elastica\Response $response Response object
*/
protected function _init(Response $response)
{
$this->_response = $response;
$result = $response->getData();
$this->_totalHits = isset($result['hits']['total']) ? $result['hits']['total'] : 0;
$this->_maxScore = isset($result['hits']['max_score']) ? $result['hits']['max_score'] : 0;
$this->_took = isset($result['took']) ? $result['took'] : 0;
$this->_timedOut = !empty($result['timed_out']);
if (isset($result['hits']['hits'])) {
foreach ($result['hits']['hits'] as $hit) {
$this->_results[] = new TransformingResult($hit, $this);
}
}
}
/**
* Returns an array of transformed results.
*
* @return object[]
*/
public function getTransformed()
{
$this->transform();
return array_map(function (TransformingResult $result) {
return $result->getTransformed();
}, $this->getResults());
}
/**
* Triggers the transformation of all Results.
*/
public function transform()
{
if ($this->transformed) {
return;
}
if (!$this->count()) {
return;
}
$this->resultTransformer->transform($this->getResults());
$this->transformed = true;
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace FOS\ElasticaBundle\Elastica;
use Elastica\Client;
use Elastica\Request;
use Elastica\Search;
use FOS\ElasticaBundle\Transformer\CombinedResultTransformer;
/**
* Overridden Elastica methods to return our TransformingResultSet
*/
class TransformingSearch extends Search
{
/**
* Search in the set indices, types
*
* @param mixed $query
* @param int|array $options OPTIONAL Limit or associative array of options (option=>value)
* @throws \Elastica\Exception\InvalidException
* @return TransformingResultSet
*/
public function search($query = '', $options = null)
{
$this->setOptionsAndQuery($options, $query);
$query = $this->getQuery();
$path = $this->getPath();
$params = $this->getOptions();
// Send scroll_id via raw HTTP body to handle cases of very large (> 4kb) ids.
if ('_search/scroll' == $path) {
$data = $params[self::OPTION_SCROLL_ID];
unset($params[self::OPTION_SCROLL_ID]);
} else {
$data = $query->toArray();
}
$response = $this->getClient()->request(
$path,
Request::GET,
$data,
$params
);
return new TransformingResultSet($response, $query, $this->_client->getResultTransformer());
}
/**
*
* @param mixed $query
* @param $fullResult (default = false) By default only the total hit count is returned. If set to true, the full ResultSet including facets is returned.
* @return int|TransformingResultSet
*/
public function count($query = '', $fullResult = false)
{
$this->setOptionsAndQuery(null, $query);
$query = $this->getQuery();
$path = $this->getPath();
$response = $this->getClient()->request(
$path,
Request::GET,
$query->toArray(),
array(self::OPTION_SEARCH_TYPE => self::OPTION_SEARCH_TYPE_COUNT)
);
$resultSet = new TransformingResultSet($response, $query, $this->_client->getResultTransformer());
return $fullResult ? $resultSet : $resultSet->getTotalHits();
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace FOS\ElasticaBundle\Elastica;
use Elastica\Document;
use Elastica\Query;
use Elastica\Request;
use Elastica\Type;
class TransformingType extends Type
{
/**
* Overridden default method that returns our TransformingResultSet.
*
* {@inheritdoc}
*/
public function moreLikeThis(Document $doc, $params = array(), $query = array())
{
$path = $doc->getId() . '/_mlt';
$query = Query::create($query);
$response = $this->request($path, Request::GET, $query->toArray(), $params);
return new TransformingResultSet($response, $query, $this->_index->getClient()->getResultTransformer());
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace FOS\ElasticaBundle\Exception;
use Exception;
class MissingModelException extends \Exception
{
public function __construct($modelCount, $resultCount)
{
$message = sprintf('Expected to have %d models, but the lookup returned %d results', $resultCount, $modelCount);
parent::__construct($message);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace FOS\ElasticaBundle\Exception;
use Exception;
class UnexpectedObjectException extends \Exception
{
public function __construct($id)
{
parent::__construct(sprintf('Lookup returned an unknown object with id %d', $id));
}
}

54
Propel/Lookup.php Normal file
View file

@ -0,0 +1,54 @@
<?php
namespace FOS\ElasticaBundle\Propel;
use Doctrine\Common\Util\Inflector;
use FOS\ElasticaBundle\Type\LookupInterface;
use FOS\ElasticaBundle\Type\TypeConfigurationInterface;
class Lookup implements LookupInterface
{
/**
* Returns the lookup key.
*
* @return string
*/
public function getKey()
{
return 'propel';
}
/**
* Look up objects of a specific type with ids as supplied.
*
* @param TypeConfigurationInterface $configuration
* @param int[] $ids
* @return object[]
*/
public function lookup(TypeConfigurationInterface $configuration, array $ids)
{
$query = $this->createQuery($configuration, $ids);
if (!$configuration->isHydrate()) {
return $query->toArray();
}
return $query->find();
}
/**
* Create a query to use in the findByIdentifiers() method.
*
* @param TypeConfigurationInterface $configuration
* @param array $ids
* @return \ModelCriteria
*/
protected function createQuery(TypeConfigurationInterface $configuration, array $ids)
{
$queryClass = $configuration->getModelClass() . 'Query';
$query = $queryClass::create();
$filterMethod = 'filterBy' . Inflector::camelize($configuration->getIdentifierProperty());
return $query->$filterMethod($ids);
}
}

View file

@ -4,9 +4,33 @@ namespace FOS\ElasticaBundle\Tests\Client;
use Elastica\Request;
use Elastica\Transport\Null as NullTransport;
use FOS\ElasticaBundle\Elastica\LoggingClient;
class LoggingClientTest extends \PHPUnit_Framework_TestCase
{
public function testOverriddenElasticaMethods()
{
$resultTransformer = $this->getMockBuilder('FOS\ElasticaBundle\Transformer\CombinedResultTransformer')
->disableOriginalConstructor()
->getMock();
$client = new LoggingClient(array(), null, $resultTransformer);
$index = $client->getIndex('index');
$type = $index->getType('type');
$this->assertInstanceOf('FOS\ElasticaBundle\Elastica\TransformingIndex', $index);
$this->assertInstanceOf('FOS\ElasticaBundle\Elastica\TransformingType', $type);
}
public function testGetResultTransformer()
{
$resultTransformer = $this->getMockBuilder('FOS\ElasticaBundle\Transformer\CombinedResultTransformer')
->disableOriginalConstructor()
->getMock();
$client = new LoggingClient(array(), null, $resultTransformer);
$this->assertSame($resultTransformer, $client->getResultTransformer());
}
public function testRequestsAreLogged()
{
$transport = new NullTransport;
@ -29,6 +53,7 @@ class LoggingClientTest extends \PHPUnit_Framework_TestCase
);
$client = $this->getMockBuilder('FOS\ElasticaBundle\Elastica\LoggingClient')
->disableOriginalConstructor()
->setMethods(array('getConnection'))
->getMock();

View file

@ -0,0 +1,42 @@
<?php
namespace FOS\ElasticaBundle\Tests\Client;
use FOS\ElasticaBundle\Elastica\TransformingIndex;
class TransformingIndexTest extends \PHPUnit_Framework_TestCase
{
/**
* @var \FOS\ElasticaBundle\Elastica\TransformingIndex
*/
private $index;
/**
* @var \FOS\ElasticaBundle\Elastica\LoggingClient
*/
private $client;
public function testCreateSearch()
{
$search = $this->index->createSearch();
$this->assertInstanceOf('FOS\ElasticaBundle\Elastica\TransformingSearch', $search);
}
public function testOverrideName()
{
$this->assertEquals('testindex', $this->index->getName());
$this->index->overrideName('newindex');
$this->assertEquals('newindex', $this->index->getName());
}
protected function setUp()
{
$this->client = $this->getMockBuilder('FOS\ElasticaBundle\Elastica\LoggingClient')
->disableOriginalConstructor()
->getMock();
$this->index = new TransformingIndex($this->client, 'testindex');
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace FOS\ElasticaBundle\Tests\Client;
use Elastica\Query;
use Elastica\Response;
use FOS\ElasticaBundle\Elastica\TransformingResult;
use FOS\ElasticaBundle\Elastica\TransformingResultSet;
class TransformingResultSetTest extends \PHPUnit_Framework_TestCase
{
public function testTransformingResult()
{
$response = new Response(array('hits' => array(
'hits' => array(
array(),
array(),
array(),
)
)));
$query = new Query();
$transformer = $this->getMockBuilder('FOS\ElasticaBundle\Transformer\CombinedResultTransformer')
->disableOriginalConstructor()
->getMock();
$resultSet = new TransformingResultSet($response, $query, $transformer);
$this->assertCount(3, $resultSet);
$this->assertInstanceOf('FOS\ElasticaBundle\Elastica\TransformingResult', $resultSet[0]);
$transformer->expects($this->once())
->method('transform')
->with($resultSet->getResults());
$resultSet->transform();
$resultSet->transform();
$this->assertSame(array(
0 => null, 1 => null, 2 => null
), $resultSet->getTransformed());
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace FOS\ElasticaBundle\Tests\Client;
use FOS\ElasticaBundle\Elastica\TransformingResult;
class TransformingResultTest extends \PHPUnit_Framework_TestCase
{
public function testTransformingResult()
{
$resultSet = $this->getMockBuilder('FOS\ElasticaBundle\Elastica\TransformingResultSet')
->disableOriginalConstructor()
->getMock();
$result = new TransformingResult(array(), $resultSet);
$resultSet->expects($this->exactly(2))
->method('transform');
$result->getTransformed();
$result->getTransformed();
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace FOS\ElasticaBundle\Transformer;
use FOS\ElasticaBundle\Elastica\TransformingResult;
/**
* Transforms results from an index wide query with multiple types.
*
* @author Tim Nagel <tim@nagel.com.au>
*/
class CombinedResultTransformer
{
/**
* @var \FOS\ElasticaBundle\Type\TypeConfigurationInterface
*/
private $configurations;
/**
* @var ResultTransformerInterface
*/
private $transformer;
/**
* @param \FOS\ElasticaBundle\Type\TypeConfigurationInterface[] $configurations
* @param ResultTransformerInterface $transformer
*/
public function __construct(array $configurations, ResultTransformerInterface $transformer)
{
$this->configurations = $configurations;
$this->transformer = $transformer;
}
/**
* Transforms Elastica results into Models.
*
* @param TransformingResult[] $results
* @return object[]
*/
public function transform($results)
{
$grouped = array();
foreach ($results as $result) {
$grouped[$result->getType()][] = $result;
}
foreach ($grouped as $type => $group) {
$this->transformer->transform($this->getConfiguration($type), $group);
}
}
/**
* Retrieves the transformer for a given type.
*
* @param string $type
* @return \FOS\ElasticaBundle\Type\TypeConfigurationInterface
* @throws \InvalidArgumentException
*/
private function getConfiguration($type)
{
if (!array_key_exists($type, $this->configurations)) {
throw new \InvalidArgumentException(sprintf(
'Configuration for type "%s" is not registered with this combined transformer.',
$type
));
}
return $this->configurations[$type];
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace FOS\ElasticaBundle\Transformer;
use FOS\ElasticaBundle\Exception\MissingModelException;
use FOS\ElasticaBundle\Exception\UnexpectedObjectException;
use FOS\ElasticaBundle\Type\LookupManager;
use FOS\ElasticaBundle\Type\TypeConfigurationInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* Handles transforming results into models.
*/
class ResultTransformer implements ResultTransformerInterface
{
/**
* @var \FOS\ElasticaBundle\Type\LookupManager
*/
private $lookupManager;
/**
* @var \Symfony\Component\PropertyAccess\PropertyAccessorInterface
*/
private $propertyAccessor;
public function __construct(
LookupManager $lookupManager,
PropertyAccessorInterface $propertyAccessor
) {
$this->lookupManager = $lookupManager;
$this->propertyAccessor = $propertyAccessor;
}
/**
* Transforms Elastica results into Models.
*
* @param TypeConfigurationInterface $configuration
* @param \FOS\ElasticaBundle\Elastica\TransformingResult[] $results
* @throws \FOS\ElasticaBundle\Exception\MissingModelException
* @throws \FOS\ElasticaBundle\Exception\UnexpectedObjectException
*/
public function transform(TypeConfigurationInterface $configuration, $results)
{
$results = $this->processResults($results);
$lookup = $this->lookupManager->getLookup($configuration->getType());
$objects = $lookup->lookup($configuration, array_keys($results));
if (!$configuration->isIgnoreMissing() and count($objects) < count($results)) {
throw new MissingModelException(count($objects), count($results));
}
$identifierProperty = $configuration->getIdentifierProperty();
foreach ($objects as $object) {
$id = $this->propertyAccessor->getValue($object, $identifierProperty);
if (!array_key_exists($id, $results)) {
throw new UnexpectedObjectException($id);
}
$results[$id]->setTransformed($object);
}
}
/**
* Processes the results array into a more usable format for the transformation.
*
* @param \FOS\ElasticaBundle\Elastica\TransformingResult[] $results
* @return \FOS\ElasticaBundle\Elastica\TransformingResult[]
*/
private function processResults($results)
{
$sorted = array();
foreach ($results as $result) {
$sorted[$result->getId()] = $result;
}
return $sorted;
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace FOS\ElasticaBundle\Transformer;
use FOS\ElasticaBundle\Type\TypeConfigurationInterface;
interface ResultTransformerInterface
{
/**
* Transforms Elastica results into Models.
*
* @param TypeConfigurationInterface $configuration
* @param array $results
*/
public function transform(TypeConfigurationInterface $configuration, $results);
}

27
Type/LookupInterface.php Normal file
View file

@ -0,0 +1,27 @@
<?php
namespace FOS\ElasticaBundle\Type;
/**
* A service that provides lookup capabilities for a type.
*
* @author Tim Nagel <tim@nagel.com.au>
*/
interface LookupInterface
{
/**
* Returns the lookup key.
*
* @return string
*/
public function getKey();
/**
* Look up objects of a specific type with ids as supplied.
*
* @param TypeConfigurationInterface $configuration
* @param int[] $ids
* @return object[]
*/
public function lookup(TypeConfigurationInterface $configuration, array $ids);
}

36
Type/LookupManager.php Normal file
View file

@ -0,0 +1,36 @@
<?php
namespace FOS\ElasticaBundle\Type;
class LookupManager
{
/**
* @var LookupInterface[]
*/
private $lookups = array();
/**
* @param LookupInterface[] $lookups
*/
public function __construct($lookups)
{
foreach ($lookups as $lookup) {
$this->lookups[$lookup->getKey()] = $lookup;
}
}
/**
* @param string $type
* @return LookupInterface
* @throws \InvalidArgumentException
*/
public function getLookup($type)
{
if (!array_key_exists($type, $this->lookups)) {
throw new \InvalidArgumentException(sprintf('Lookup with key "%s" does not exist', $type));
}
return $this->lookups[$type];
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace FOS\ElasticaBundle\Type;
/**
* A data object that contains configuration information about a specific type.
*
* @author Tim Nagel <tim@nagel.com.au>
*/
interface TypeConfigurationInterface
{
/**
* The identifier property that is used to retrieve an identifier from the model.
*
* @return string
*/
public function getIdentifierProperty();
/**
* Returns the fully qualified class for the model that this type represents.
*
* @return string
*/
public function getModelClass();
/**
* Returns the repository method that will create a query builder or associated
* query object for lookup purposes.
*
* @return string
*/
public function getRepositoryMethod();
/**
* Returns the name of the type.
*
* @return string
*/
public function getType();
/**
* If the lookup should hydrate models to objects or leave data as an array.
*
* @return bool
*/
public function isHydrate();
/**
* If the type should ignore missing results from a lookup.
*
* @return bool
*/
public function isIgnoreMissing();
}

View file

@ -12,6 +12,7 @@
],
"require": {
"php": ">=5.3.2",
"doctrine/inflector": "~1.0",
"symfony/framework-bundle": "~2.3",
"symfony/console": "~2.1",
"symfony/form": "~2.1",