Skip to content

Commit

Permalink
custom functions: implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
hrach committed Aug 20, 2017
1 parent fa4077b commit b9404cf
Show file tree
Hide file tree
Showing 31 changed files with 1,052 additions and 419 deletions.
1 change: 1 addition & 0 deletions doc/migrate_3.0.texy
Expand Up @@ -2,3 +2,4 @@ Migration Guide for 3.0
#######################

- Entity: all datetime properties (types) have to be replaced with `\DateTimeImmutable` type.
- Collection: removed comparison operator `!`, use equivalent `!=` operator, e.g. replace `findBy(['name!' => 'Jon'])` with `findBy(['name!=' => 'Jon'])`.
21 changes: 19 additions & 2 deletions src/Collection/ArrayCollection.php
Expand Up @@ -40,7 +40,10 @@ class ArrayCollection implements ICollection
/** @var ArrayCollectionHelper */
protected $helper;

/** @var array */
/** @var callable[] */
protected $collectionFunctions = [];

/** @var callable[] */
protected $collectionFilter = [];

/** @var array */
Expand Down Expand Up @@ -104,6 +107,14 @@ public function limitBy(int $limit, int $offset = null): ICollection
}


public function applyFunction(string $functionName, ...$args): ICollection
{
$collection = clone $this;
$collection->collectionFunctions[] = $this->getHelper()->createFunction($functionName, $args);
return $collection;
}


public function fetch()
{
if (!$this->fetchIterator) {
Expand Down Expand Up @@ -219,8 +230,13 @@ public function __clone()

protected function processData()
{
if ($this->collectionFilter || $this->collectionSorter || $this->collectionLimit) {
if ($this->collectionFunctions || $this->collectionFilter || $this->collectionSorter || $this->collectionLimit) {
$data = $this->data;

foreach ($this->collectionFunctions as $function) {
$data = $function($data);
}

foreach ($this->collectionFilter as $filter) {
$data = array_filter($data, $filter);
}
Expand All @@ -234,6 +250,7 @@ protected function processData()
$data = array_slice($data, $this->collectionLimit[1] ?: 0, $this->collectionLimit[0]);
}

$this->collectionFunctions = [];
$this->collectionFilter = [];
$this->collectionSorter = [];
$this->collectionLimit = null;
Expand Down
6 changes: 6 additions & 0 deletions src/Collection/EmptyCollection.php
Expand Up @@ -49,6 +49,12 @@ public function limitBy(int $limit, int $offset = null): ICollection
}


public function applyFunction(string $functionName, ...$args): ICollection
{
return clone $this;
}


public function fetch()
{
return null;
Expand Down
254 changes: 90 additions & 164 deletions src/Collection/Helpers/ArrayCollectionHelper.php
Expand Up @@ -15,194 +15,75 @@
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Entity\Reflection\EntityMetadata;
use Nextras\Orm\Entity\Reflection\PropertyMetadata;
use Nextras\Orm\InvalidArgumentException;
use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata;
use Nextras\Orm\InvalidStateException;
use Nextras\Orm\NotSupportedException;
use Nextras\Orm\Relationships\IRelationshipCollection;
use Nextras\Orm\Mapper\IMapper;
use Nextras\Orm\Mapper\Memory\CustomFunctions\IArrayFunction;
use Nextras\Orm\Mapper\Memory\CustomFunctions\IArrayFilterFunction;
use Nextras\Orm\Repository\Functions\ConjunctionOperatorFunction;
use Nextras\Orm\Repository\Functions\DisjunctionOperatorFunction;
use Nextras\Orm\Repository\Functions\ValueOperatorFunction;
use Nextras\Orm\Repository\IRepository;


class ArrayCollectionHelper
{

/** @var IRepository */
private $repository;

/** @var IMapper */
private $mapper;


public function __construct(IRepository $repository)
{
$this->repository = $repository;
$this->mapper = $repository->getMapper();
}


public function createFilter(array $conditions): Closure
public function createFunction(string $function, array $expr): Closure
{
if (!isset($conditions[0])) {
$operator = ICollection::AND;
} else {
$operator = array_shift($conditions);
}

$callbacks = [];
foreach ($conditions as $expression => $value) {
if (is_int($expression)) {
$callbacks[] = $this->createFilter($value);
} else {
$callbacks[] = $this->createExpressionFilter($expression, $value);
}
$customFunction = $this->getFunction($function);
if (!$customFunction instanceof IArrayFunction) {
throw new InvalidStateException("Custom function $function has to implement IQueryBuilderFunction interface.");
}

if ($operator === ICollection::AND) {
return function ($value) use ($callbacks) {
foreach ($callbacks as $callback) {
if (!$callback($value)) {
return false;
}
}
return true;
};
} elseif ($operator === ICollection::OR) {
return function ($value) use ($callbacks) {
foreach ($callbacks as $callback) {
if ($callback($value)) {
return true;
}
}
return false;
};
} else {
throw new NotSupportedException("Operator $operator is not supported");
}
return function (array $entities) use ($customFunction, $expr) {
/** @var IEntity[] $entities */
return $customFunction->processArrayFilter($this, $entities, $expr);
};
}


/**
* @param mixed $value
*/
public function createExpressionFilter(string $condition, $value): Closure
public function createFilter(array $expr): Closure
{
list($chain, $operator, $sourceEntity) = ConditionParserHelper::parseCondition($condition);
$sourceEntityMeta = $this->repository->getEntityMetadata($sourceEntity);
$function = isset($expr[0]) ? array_shift($expr) : ICollection::AND;
$customFunction = $this->getFunction($function);

if ($value instanceof IEntity) {
$value = $value->getValue('id');
if (!$customFunction instanceof IArrayFilterFunction) {
throw new InvalidStateException("Custom function $function has to implement IQueryBuilderFilterFunction interface.");
}

$comparator = $this->createComparator($operator, is_array($value));
return $this->createFilterEvaluator($chain, $comparator, $sourceEntityMeta, $value);
}


private function createComparator(string $operator, bool $isArray): Closure
{
if ($operator === ConditionParserHelper::OPERATOR_EQUAL) {
if ($isArray) {
return function ($property, $value) {
return in_array($property, $value, true);
};
} else {
return function ($property, $value) {
return $property === $value;
};
}
} elseif ($operator === ConditionParserHelper::OPERATOR_NOT_EQUAL) {
if ($isArray) {
return function ($property, $value) {
return !in_array($property, $value, true);
};
} else {
return function ($property, $value) {
return $property !== $value;
};
}
} elseif ($operator === ConditionParserHelper::OPERATOR_GREATER) {
return function ($property, $value) {
return $property > $value;
};
} elseif ($operator === ConditionParserHelper::OPERATOR_EQUAL_OR_GREATER) {
return function ($property, $value) {
return $property >= $value;
};
} elseif ($operator === ConditionParserHelper::OPERATOR_SMALLER) {
return function ($property, $value) {
return $property < $value;
};
} elseif ($operator === ConditionParserHelper::OPERATOR_EQUAL_OR_SMALLER) {
return function ($property, $value) {
return $property <= $value;
};
} else {
throw new InvalidArgumentException();
}
}


protected function createFilterEvaluator(array $chainSource, Closure $predicate, EntityMetadata $sourceEntityMetaSource, $targetValue): Closure
{
$evaluator = function (
IEntity $element,
array $chain = null,
EntityMetadata $sourceEntityMeta = null
) use (
& $evaluator,
$predicate,
$chainSource,
$sourceEntityMetaSource,
$targetValue
): bool {
if (!$chain) {
$sourceEntityMeta = $sourceEntityMetaSource;
$chain = $chainSource;
}

$column = array_shift($chain);
$propertyMeta = $sourceEntityMeta->getProperty($column); // check if property exists
$value = $element->hasValue($column) ? $element->getValue($column) : null;

if (!$chain) {
if ($column === 'id' && count($sourceEntityMeta->getPrimaryKey()) > 1 && !isset($targetValue[0][0])) {
$targetValue = [$targetValue];
}
return $predicate(
$this->normalizeValue($value, $propertyMeta),
$this->normalizeValue($targetValue, $propertyMeta)
);
}

$targetEntityMeta = $propertyMeta->relationship->entityMetadata;
if ($value === null) {
return false;

} elseif ($value instanceof IRelationshipCollection) {
foreach ($value as $node) {
if ($evaluator($node, $chain, $targetEntityMeta)) {
return true;
}
}

return false;
} else {
return $evaluator($value, $chain, $targetEntityMeta);
}
return function (IEntity $entity) use ($customFunction, $expr) {
return $customFunction->processArrayFilter($this, $entity, $expr);
};

return $evaluator;
}


public function createSorter(array $conditions): Closure
{
$columns = [];
foreach ($conditions as $pair) {
list($column, , $sourceEntity) = ConditionParserHelper::parseCondition($pair[0]);
list($column, $sourceEntity) = ConditionParserHelper::parsePropertyExpr($pair[0]);
$sourceEntityMeta = $this->repository->getEntityMetadata($sourceEntity);
$columns[] = [$column, $pair[1], $sourceEntityMeta];
}

return function ($a, $b) use ($columns) {
foreach ($columns as $pair) {
$_a = $this->getter($a, $pair[0], $pair[2]);
$_b = $this->getter($b, $pair[0], $pair[2]);
$_a = $this->getValueByTokens($a, $pair[0], $pair[2])->value;
$_b = $this->getValueByTokens($b, $pair[0], $pair[2])->value;
$direction = $pair[1] === ICollection::ASC ? 1 : -1;

if ($_a === null || $_b === null) {
Expand Down Expand Up @@ -230,26 +111,15 @@ public function createSorter(array $conditions): Closure
}


public function getter(IEntity $element, array $chain, EntityMetadata $sourceEntityMeta)
public function getValue(IEntity $entity, string $expr): ValueReference
{
$column = array_shift($chain);
$propertyMeta = $sourceEntityMeta->getProperty($column); // check if property exists
$value = $element->hasValue($column) ? $element->getValue($column) : null;

if ($value instanceof IRelationshipCollection) {
throw new InvalidStateException('You can not sort by hasMany relationship.');
}

if (!$chain) {
return $this->normalizeValue($value, $propertyMeta);
} else {
$targetEntityMeta = $propertyMeta->relationship->entityMetadata;
return $value ? $this->getter($value, $chain, $targetEntityMeta) : null;
}
list($tokens, $sourceEntityClassName) = ConditionParserHelper::parsePropertyExpr($expr);
$sourceEntityMeta = $this->repository->getEntityMetadata($sourceEntityClassName);
return $this->getValueByTokens($entity, $tokens, $sourceEntityMeta);
}


private function normalizeValue($value, PropertyMetadata $propertyMetadata)
public function normalizeValue($value, PropertyMetadata $propertyMetadata)
{
if ($value instanceof IEntity) {
return $value->hasValue('id') ? $value->getValue('id') : null;
Expand All @@ -263,4 +133,60 @@ private function normalizeValue($value, PropertyMetadata $propertyMetadata)

return $value;
}


private function getValueByTokens(IEntity $entity, array $tokens, EntityMetadata $sourceEntityMeta): ValueReference
{
$isFromHasManyResult = false;
$values = [];
$stack = [[$entity, $tokens, $sourceEntityMeta]];

do {
/** @var IEntity $value */
/** @var string[] $tokens */
/** @var EntityMetadata $entityMeta */
list ($value, $tokens, $entityMeta) = array_shift($stack);

do {
$propertyName = array_shift($tokens);
$propertyMeta = $entityMeta->getProperty($propertyName); // check if property exists
$value = $value->hasValue($propertyName) ? $value->getValue($propertyName) : null;

if ($propertyMeta->relationship) {
$entityMeta = $propertyMeta->relationship->entityMetadata;

if (
$propertyMeta->relationship->type === PropertyRelationshipMetadata::MANY_HAS_MANY
|| $propertyMeta->relationship->type === PropertyRelationshipMetadata::ONE_HAS_MANY
) {
$isFromHasManyResult = true;
foreach ($value as $subEntity) {
$stack[] = [$subEntity, $tokens, $entityMeta];
}
continue 2;
}
}

} while (count($tokens) > 0 && $value !== null);

$values[] = $this->normalizeValue($value, $propertyMeta);
} while (!empty($stack));

return new ValueReference($isFromHasManyResult, $isFromHasManyResult ? $values : $values[0], $propertyMeta);
}


// todo: optimize
private function getFunction(string $operator)
{
if ($operator === ValueOperatorFunction::class) {
return new ValueOperatorFunction();
} elseif ($operator === ConjunctionOperatorFunction::class) {
return new ConjunctionOperatorFunction();
} elseif ($operator === DisjunctionOperatorFunction::class) {
return new DisjunctionOperatorFunction();
} else {
return $this->repository->getCustomFunction($operator);
}
}
}

0 comments on commit b9404cf

Please sign in to comment.