Skip to content

Commit

Permalink
Merge pull request #265 from analogueorm/fix/inheritance-eager-loading
Browse files Browse the repository at this point in the history
Fix/inheritance eager loading
  • Loading branch information
RemiCollin authored May 28, 2018
2 parents 8c94143 + b6d334b commit 41d7556
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 132 deletions.
2 changes: 1 addition & 1 deletion src/Relationships/EmbeddedRelationship.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace Analogue\ORM\Relationships;

use Analogue\ORM\System\Builders\ResultBuilder;
use Analogue\ORM\System\Manager;
use Analogue\ORM\System\ResultBuilder;
use Analogue\ORM\System\Wrappers\Factory;

abstract class EmbeddedRelationship
Expand Down
3 changes: 2 additions & 1 deletion src/System/EntityBuilder.php → src/System/Builders/EntityBuilder.php
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<?php

namespace Analogue\ORM\System;
namespace Analogue\ORM\System\Builders;

use Analogue\ORM\System\Mapper;
use Analogue\ORM\System\Wrappers\Factory;

/**
Expand Down
115 changes: 115 additions & 0 deletions src/System/Builders/PolymorphicResultBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace Analogue\ORM\System\Builders;

use Analogue\ORM\System\Manager;
use Analogue\ORM\System\Mapper;

class PolymorphicResultBuilder implements ResultBuilderInterface
{
/**
* The default mapper used to build entities with.
*
* @var \Analogue\ORM\System\Mapper
*/
protected $defaultMapper;

/**
* Reference to all mappers used in this result set.
*
* @var array
*/
protected $mappers = [];

/**
* Relations that will be eager loaded on this query.
*
* @var array
*/
protected $eagerLoads;

/**
* The Entity Map for the entity to build.
*
* @var \Analogue\ORM\EntityMap
*/
protected $entityMap;

/**
* An array of builders used by this class to build necessary
* entities for each result type.
*
* @var array
*/
protected $builders = [];

/**
* ResultBuilder constructor.
*
* @param Mapper $defaultMapper
*/
public function __construct(Mapper $defaultMapper)
{
$this->defaultMapper = $defaultMapper;
$this->entityMap = $defaultMapper->getEntityMap();
}

/**
* Convert a result set into an array of entities.
*
* @param array $results
* @param array $eagerLoads name of the relation(s) to be eager loaded on the Entities
*
* @return array
*/
public function build(array $results, array $eagerLoads)
{
// Make a list of all primary key of the current result set. This will
// allow us to group all polymorphic operations by type, then put
// back every object in the intended order.
$primaryKeyColumn = $this->entityMap->getKeyName();
$ids = array_map(function ($row) use ($primaryKeyColumn) {
return $row[$primaryKeyColumn];
}, $results);

$results = array_combine($ids, $results);

// Make a list of types appearing within this result set.
$discriminatorColumn = $this->entityMap->getDiscriminatorColumn();
$types = array_unique(array_pluck($results, $discriminatorColumn));

// We'll split the result set by type that will make it easier to deal
// with.
$entities = [];

foreach ($types as $type) {
$this->mappers[$type] = $this->getMapperForType($type);

$resultsByType[$type] = array_filter($results, function (array $row) use ($type, $discriminatorColumn) {
return $row[$discriminatorColumn] === $type;
});

$entities = $entities + $this->buildResultsForType($resultsByType[$type], $type, $eagerLoads);
}

return array_map(function ($id) use ($entities) {
return $entities[$id];
}, $ids);
}

protected function buildResultsForType($results, $type, array $eagerLoads)
{
$builder = new ResultBuilder($this->mappers[$type]);

return $builder->build($results, $eagerLoads);
}

protected function getMapperForType(string $type) : Mapper
{
$columnMap = $this->entityMap->getDiscriminatorColumnMap();

$class = isset($columnMap[$type]) ? $columnMap[$type] : $type;

return Manager::getInstance()->mapper($class);
}
}
149 changes: 27 additions & 122 deletions src/System/ResultBuilder.php → src/System/Builders/ResultBuilder.php
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
<?php

namespace Analogue\ORM\System;
namespace Analogue\ORM\System\Builders;

use Analogue\ORM\Relationships\Relationship;
use Analogue\ORM\System\Mapper;
use Closure;

class ResultBuilder
class ResultBuilder implements ResultBuilderInterface
{
/**
* The default mapper used to build entities with.
* The mapper used to build entities with.
*
* @var \Analogue\ORM\System\Mapper
*/
protected $defaultMapper;
protected $mapper;

/**
* Relations that will be eager loaded on this query.
Expand All @@ -39,19 +40,19 @@ class ResultBuilder
/**
* ResultBuilder constructor.
*
* @param Mapper $defaultMapper
* @param Mapper $mapper
*/
public function __construct(Mapper $defaultMapper)
public function __construct(Mapper $mapper)
{
$this->defaultMapper = $defaultMapper;
$this->entityMap = $defaultMapper->getEntityMap();
$this->mapper = $mapper;
$this->entityMap = $mapper->getEntityMap();
}

/**
* Convert a result set into an array of entities.
*
* @param array $results
* @param array $eagerLoads name of the relation to be eager loaded on the Entities
* @param array $eagerLoads name of the relation(s) to be eager loaded on the Entities
*
* @return array
*/
Expand All @@ -68,17 +69,7 @@ public function build(array $results, array $eagerLoads)
// current result set to these loaded relationships.
$results = $this->queryEagerLoadedRelationships($results, $eagerLoads);

// Note : Maybe we could use a PolymorphicResultBuilder, which would
// be shared by both STI and polymorphic relations, as they share the
// same process.

switch ($this->entityMap->getInheritanceType()) {
case 'single_table':
return $this->buildUsingSingleTableInheritance($results);

default:
return $this->buildWithDefaultMapper($results);
}
return $this->buildResultSet($results);
}

/**
Expand All @@ -90,40 +81,11 @@ public function build(array $results, array $eagerLoads)
*/
protected function cacheResults(array $results)
{
switch ($this->entityMap->getInheritanceType()) {
case 'single_table':

$this->cacheSingleTableInheritanceResults($results);
break;

default:
$mapper = $this->defaultMapper;
// When hydrating EmbeddedValue object, they'll likely won't
// have a primary key set.
if (!is_null($mapper->getEntityMap()->getKeyName())) {
$mapper->getEntityCache()->add($results);
}
break;
}
}

/**
* Cache results from a STI result set.
*
* @param array $results
*
* @return void
*/
protected function cacheSingleTableInheritanceResults(array $results)
{
foreach ($results as $result) {
$mapper = $this->getMapperForSingleRow($result);

// When hydrating EmbeddedValue object, they'll likely won't
// have a primary key set.
if (!is_null($mapper->getEntityMap()->getKeyName())) {
$mapper->getEntityCache()->add([$result]);
}
$mapper = $this->mapper;
// When hydrating EmbeddedValue object, they'll likely won't
// have a primary key set.
if (!is_null($mapper->getEntityMap()->getKeyName())) {
$mapper->getEntityCache()->add($results);
}
}

Expand All @@ -137,7 +99,7 @@ protected function cacheSingleTableInheritanceResults(array $results)
protected function buildEmbeddedRelationships(array $results) : array
{
$entityMap = $this->entityMap;
$instance = $this->defaultMapper->newInstance();
$instance = $this->mapper->newInstance();
$embeddeds = $entityMap->getEmbeddedRelationships();

foreach ($embeddeds as $embedded) {
Expand Down Expand Up @@ -232,6 +194,11 @@ protected function parseNested(string $name, array $results): array
public function eagerLoadRelations(array $results): array
{
foreach ($this->eagerLoads as $name => $constraints) {
// First, we'll check if the entity map has a relation and just pass if it
// is not the case
if (!in_array($name, $this->entityMap->getRelationships())) {
continue;
}

// For nested eager loads we'll skip loading them here and they will be set as an
// eager load on the query to retrieve the relation so that they will be eager
Expand Down Expand Up @@ -278,13 +245,13 @@ protected function loadRelation(array $results, string $name, Closure $constrain
*
* @return \Analogue\ORM\Relationships\Relationship
*/
public function getRelation(string $relation): Relationship
public function getRelation(string $relation) : Relationship
{
// We want to run a relationship query without any constrains so that we will
// not have to remove these where clauses manually which gets really hacky
// and is error prone while we remove the developer's own where clauses.
$query = Relationship::noConstraints(function () use ($relation) {
return $this->entityMap->$relation($this->defaultMapper->newInstance());
return $this->entityMap->$relation($this->mapper->newInstance());
});

$nested = $this->nestedRelations($relation);
Expand Down Expand Up @@ -338,80 +305,18 @@ protected function isNested(string $name, string $relation): bool
}

/**
* Build an entity from results, using the default mapper on this builder.
* This is the default build plan when no table inheritance is being used.
* Build an entity from results.
*
* @param array $results
*
* @return array
*/
protected function buildWithDefaultMapper(array $results): array
protected function buildResultSet(array $results): array
{
$builder = new EntityBuilder($this->defaultMapper, array_keys($this->eagerLoads));
$builder = new EntityBuilder($this->mapper, array_keys($this->eagerLoads));

return array_map(function ($item) use ($builder) {
return $builder->build($item);
}, $results);
}

/**
* Build an entity from results, using single table inheritance.
*
* @param array $results
*
* @return array
*/
protected function buildUsingSingleTableInheritance(array $results): array
{
return array_map(function ($item) {
$builder = $this->builderForResult($item);

return $builder->build($item);
}, $results);
}

/**
* Given a result array, return the entity builder needed to correctly
* build the result into an entity. If no getDiscriminatorColumnMap property
* has been defined on the EntityMap, we'll assume that the value stored in
* the $type column is the fully qualified class name of the entity and
* we'll use it instead.
*
* @param array $result
*
* @return EntityBuilder
*/
protected function builderForResult(array $result): EntityBuilder
{
$type = $result[$this->entityMap->getDiscriminatorColumn()];

$mapper = $this->getMapperForSingleRow($result);

if (!isset($this->builders[$type])) {
$this->builders[$type] = new EntityBuilder(
$mapper,
array_keys($this->eagerLoads)
);
}

return $this->builders[$type];
}

/**
* Get mapper corresponding to the result type.
*
* @param array $result
*
* @return Mapper
*/
protected function getMapperForSingleRow(array $result) : Mapper
{
$type = $result[$this->entityMap->getDiscriminatorColumn()];

$columnMap = $this->entityMap->getDiscriminatorColumnMap();

$class = isset($columnMap[$type]) ? $columnMap[$type] : $type;

return Manager::getInstance()->mapper($class);
}
}
18 changes: 18 additions & 0 deletions src/System/Builders/ResultBuilderFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Analogue\ORM\System\Builders;

use Analogue\ORM\System\Mapper;

class ResultBuilderFactory
{
public function make(Mapper $mapper) : ResultBuilderInterface
{
switch ($mapper->getEntityMap()->getInheritanceType()) {
case 'single_table':
return new PolymorphicResultBuilder($mapper);
default:
return new ResultBuilder($mapper);
}
}
}
8 changes: 8 additions & 0 deletions src/System/Builders/ResultBuilderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Analogue\ORM\System\Builders;

interface ResultBuilderInterface
{
public function build(array $results, array $eagerLoads);
}
Loading

0 comments on commit 41d7556

Please sign in to comment.