Skip to content

Commit

Permalink
feat: query terms with post relationship condition
Browse files Browse the repository at this point in the history
  • Loading branch information
williarin committed Mar 17, 2022
1 parent f858e7a commit a09663d
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 41 deletions.
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ reset-woocommerce:
@$(CLI) wc shop_order create --user=1 --customer_id=0 --line_items='[{"product_id":20},{"product_id":22}]' \
--billing='{"first_name":"Trudie","last_name":"Metz","company":"Amazon","address_1":"135 Wyandot Ave","city":"Marion","state":"Ohio","postcode":"43302","country":"United States","email":"[email protected]","phone":"(740) 383-4031"}' \
--shipping='{"first_name":"Trudie","last_name":"Metz","company":"Amazon","address_1":"135 Wyandot Ave","city":"Marion","state":"Ohio","postcode":"43302","country":"United States","email":"[email protected]","phone":"(740) 383-4031"}'
@$(CLI) wc product_attribute create --name="Manufacturer" --user=1
@$(CLI) wc product_attribute_term create 1 --name="SuperBrand" --user=1
@$(CLI) wc product_attribute_term create 1 --name="MegaBrand" --user=1
@$(CLI) wc product create --name="Special Forces Hoodie" --description="Military hoodie with logo" \
--type=simple --regular_price=500 --user=1 --categories='[{"id": 17}]' --sku="super-forces-hoodie" \
--attributes='[{"name": "pa_manufacturer", "visible": true, "options": ["MegaBrand"]}]'
@$(CLI) post term add 64 pa_manufacturer megabrand

.PHONY: test
test:
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,27 @@ $products = $manager->getRepository(Product::class)
]);
```

### Post relationship conditions

Query terms based on their posts relationships.

```php
// Fetch all terms of the product with SKU "super-forces-hoodie"
// belonging to all taxonomies except "product_tag", "product_type", "product_visibility".
$terms = $manager->getRepository(Term::class)
->findBy([
new SelectColumns(['taxonomy', 'name']),
new PostRelationshipCondition(Product::class, [
'post_status' => new Operand(['publish', 'private'], Operand::OPERATOR_IN),
'sku' => 'super-forces-hoodie',
]),
'taxonomy' => new Operand(
['product_tag', 'product_type', 'product_visibility'],
Operand::OPERATOR_NOT_IN,
),
]);
```

### Restrict selected columns

Querying all columns at once is slow, especially if you have a lot of entities to retrieve.
Expand Down
128 changes: 120 additions & 8 deletions src/Bridge/Repository/AbstractEntityRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Williarin\WordpressInterop\Bridge\Entity\BaseEntity;
use Williarin\WordpressInterop\Criteria\NestedCondition;
use Williarin\WordpressInterop\Criteria\Operand;
use Williarin\WordpressInterop\Criteria\PostRelationshipCondition;
use Williarin\WordpressInterop\Criteria\RelationshipCondition;
use Williarin\WordpressInterop\Criteria\SelectColumns;
use Williarin\WordpressInterop\Criteria\TermRelationshipCondition;
Expand Down Expand Up @@ -160,17 +161,22 @@ protected function doUpdate(string $name, array $arguments): bool
return $this->updateSingleField($arguments[0], property_to_field(substr($name, 6)), $arguments[1]);
}

protected function getMappedMetaKey(mixed $fieldName): string
protected function getMappedMetaKey(mixed $fieldName, string $entityClassName = null): string
{
$mappedFields = $entityClassName ? (new \ReflectionClassConstant(
$this->entityManager->getRepository($entityClassName),
'MAPPED_FIELDS',
))->getValue() : static::MAPPED_FIELDS;

if (
!is_array(static::MAPPED_FIELDS)
|| empty(static::MAPPED_FIELDS)
|| !in_array($fieldName, static::MAPPED_FIELDS, true)
!is_array($mappedFields)
|| empty($mappedFields)
|| !in_array($fieldName, $mappedFields, true)
) {
return $fieldName;
}

$key = array_search($fieldName, static::MAPPED_FIELDS, true);
$key = array_search($fieldName, $mappedFields, true);

if (is_numeric($key)) {
return sprintf('_%s', $fieldName);
Expand Down Expand Up @@ -234,6 +240,12 @@ protected function handleSpecialCriteria(
return self::IS_SPECIAL_CRITERIA;
}

if ($value instanceof PostRelationshipCondition) {
$this->createPostRelationshipCriteria($queryBuilder, $value);

return self::IS_SPECIAL_CRITERIA;
}

return 0;
}

Expand All @@ -242,6 +254,8 @@ protected function handleRegularCriteria(
array $criteria,
string $field,
mixed $value,
string $entityClassName = null,
int $aliasNumber = null,
): void {
$snakeField = u($field)
->snake()
Expand Down Expand Up @@ -278,7 +292,23 @@ protected function handleRegularCriteria(
}
}

if (in_array(substr($field, (strpos($field, '.') ?: -1) + 1), $this->getEntityExtraFields(), true)) {
if (
$entityClassName !== null
&& $aliasNumber !== null
&& in_array(
substr($field, (strpos($field, '.') ?: -1) + 1),
$this->getEntityExtraFields($entityClassName),
true,
)
) {
$exprKey = sprintf('pm_%d.meta_key = :%s_key', $aliasNumber, $snakeField);
$queryBuilder->andWhere($exprKey)
->setParameter(sprintf('%s_key', $snakeField), $this->getMappedMetaKey($field, $entityClassName))
;

$exprValue = sprintf('pm_%d.meta_value %s %s', $aliasNumber, $operator, $parameter);
$queryBuilder->andWhere($exprValue);
} elseif (in_array(substr($field, (strpos($field, '.') ?: -1) + 1), $this->getEntityExtraFields(), true)) {
$expr = sprintf('%s %s %s', $field, $operator, $parameter);
$queryBuilder->andHaving($expr);
} else {
Expand Down Expand Up @@ -363,6 +393,71 @@ private function createTermRelationshipCriteria(
++$aliasNumber;
}

private function createPostRelationshipCriteria(
QueryBuilder $queryBuilder,
PostRelationshipCondition $condition,
): void {
static $aliasNumber = 0;

$queryBuilder
->join(
'tt',
$this->entityManager->getTablesPrefix() . 'term_relationships',
sprintf('tr_%d', $aliasNumber),
sprintf('tt.term_taxonomy_id = tr_%s.term_taxonomy_id', $aliasNumber),
)
->join(
sprintf('tr_%d', $aliasNumber),
$this->entityManager->getTablesPrefix() . 'posts',
sprintf('p_%d', $aliasNumber),
sprintf('tr_%d.object_id = p_%d.id', $aliasNumber, $aliasNumber),
)
;

$this->addPostMetaJoinForPostRelationshipCondition($queryBuilder, $condition, $aliasNumber);
$prefixedCriteria = $this->getPrefixedCriteriaForPostRelationshipCondition($condition, $aliasNumber);
$normalizedCriteria = $this->normalizeCriteria($prefixedCriteria, $condition->getEntityClassName());

foreach ($normalizedCriteria as $field => $value) {
$this->handleRegularCriteria(
$queryBuilder,
$prefixedCriteria,
$field,
$value,
$condition->getEntityClassName(),
$aliasNumber,
);
}

++$aliasNumber;
}

private function addPostMetaJoinForPostRelationshipCondition(
QueryBuilder $queryBuilder,
PostRelationshipCondition $condition,
int $aliasNumber,
): void {
$extraFields = $this->getEntityExtraFields($condition->getEntityClassName());

if (!empty($extraFields)) {
$alias = sprintf('pm_%d', $aliasNumber);
$this->tableAliases[$alias] = [];

$queryBuilder->leftJoin(
sprintf('p_%d', $aliasNumber),
$this->entityManager->getTablesPrefix() . 'postmeta',
$alias,
sprintf('p_%d.id = pm_%d.post_id', $aliasNumber, $aliasNumber),
);

foreach ($extraFields as $extraField) {
$this->tableAliases[$alias][] = property_to_field($extraField);
}

$queryBuilder->addGroupBy(sprintf('p_%d.id', $aliasNumber));
}
}

private function getPrefixedCriteriaForTermRelationshipCondition(array $criteria, int $aliasNumber): array
{
$output = [];
Expand All @@ -379,6 +474,25 @@ private function getPrefixedCriteriaForTermRelationshipCondition(array $criteria
return $output;
}

private function getPrefixedCriteriaForPostRelationshipCondition(
PostRelationshipCondition $condition,
int $aliasNumber
): array {
$output = [];

foreach ($condition->getCriteria() as $field => $value) {
$prefixedField = $field;

if (in_array($field, $this->getEntityBaseFields($condition->getEntityClassName()), true)) {
$prefixedField = sprintf('p_%d.%s', $aliasNumber, $field);
}

$output[$prefixedField] = $value;
}

return $output;
}

private function selectColumns(QueryBuilder $queryBuilder, array $extraFields, SelectColumns $value): void
{
$selects = [];
Expand Down Expand Up @@ -407,8 +521,6 @@ private function selectColumns(QueryBuilder $queryBuilder, array $extraFields, S
$queryBuilder->select(...$selects);

if (!$hasExtraFields) {
$queryBuilder->resetQueryPart('groupBy');

$joinQueryPart = $queryBuilder->getQueryPart('join');

if (empty($joinQueryPart)) {
Expand Down
16 changes: 13 additions & 3 deletions src/Bridge/Repository/FieldValidationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,20 @@ private function validateFieldName(string $fieldName, string $fallbackEntity, st
return $expectedType->getName();
}

private function validateFieldValue(string $field, mixed $value): mixed
private function validateFieldValue(string $field, mixed $value, string $entityClassName = null): mixed
{
$fallbackEntity = get_parent_class(static::class) ?: static::class;
$expectedType = $this->validateFieldName($field, $fallbackEntity, static::TABLE_NAME);
if (!$entityClassName) {
$fallbackEntity = get_parent_class(static::class) ?: static::class;
$expectedType = $this->validateFieldName($field, $fallbackEntity, static::TABLE_NAME);
} else {
$expectedType = $this->validateFieldName(
$field,
$entityClassName,
(new \ReflectionClassConstant($this->entityManager->getRepository($entityClassName), 'TABLE_NAME'))
->getValue()
);
}

$resolvedValue = $value instanceof Operand ? $value->getOperand() : $value;

if ($value instanceof Operand && $value->isLooseOperator()) {
Expand Down
52 changes: 29 additions & 23 deletions src/Bridge/Repository/FindByTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ trait FindByTrait
{
use NormalizerTrait;

protected ?array $entityBaseFields = null;
protected ?array $entityExternalFields = null;
protected ?array $entityExtraFields = null;
protected array $entityBaseFields = [];
protected array $entityExternalFields = [];
protected array $entityExtraFields = [];

public function find(int $id): mixed
{
Expand Down Expand Up @@ -72,47 +72,53 @@ public function findBy(array $criteria, array $orderBy = null, ?int $limit = nul
/**
* @return string[]
*/
protected function getEntityBaseFields(): array
protected function getEntityBaseFields(string $entityClassName = null): array
{
if ($this->entityBaseFields === null) {
$entityClassName = $this->getEntityClassName();
$this->entityBaseFields = array_keys($this->serializer->normalize(new $entityClassName(), null, [
'groups' => ['base'],
]));
$entityClassName = $entityClassName ?? $this->getEntityClassName();

if (!array_key_exists($entityClassName, $this->entityBaseFields)) {
$this->entityBaseFields[$entityClassName] = array_keys(
$this->serializer->normalize(new $entityClassName(), null, [
'groups' => ['base'],
])
);
}

return $this->entityBaseFields;
return $this->entityBaseFields[$entityClassName];
}

protected function getExternalFields(): array
protected function getExternalFields(string $entityClassName = null): array
{
if ($this->entityExternalFields === null) {
$this->entityExternalFields = [];
$entityClassName = $entityClassName ?? $this->getEntityClassName();

foreach ((new \ReflectionClass($this->getEntityClassName()))->getProperties() as $property) {
if (!array_key_exists($entityClassName, $this->entityExternalFields)) {
$this->entityExternalFields[$entityClassName] = [];

foreach ((new \ReflectionClass($entityClassName))->getProperties() as $property) {
if (\count($property->getAttributes(External::class)) > 0) {
$this->entityExternalFields[] = property_to_field($property->getName());
$this->entityExternalFields[$entityClassName][] = property_to_field($property->getName());
}
}
}

return $this->entityExternalFields;
return $this->entityExternalFields[$entityClassName];
}

/**
* @return string[]
*/
protected function getEntityExtraFields(): array
protected function getEntityExtraFields(string $entityClassName = null): array
{
if ($this->entityExtraFields === null) {
$baseFields = $this->getEntityBaseFields();
$externalFields = $this->getExternalFields();
$entityClassName = $this->getEntityClassName();
$entityClassName = $entityClassName ?? $this->getEntityClassName();

if (!array_key_exists($entityClassName, $this->entityExtraFields)) {
$baseFields = $this->getEntityBaseFields($entityClassName);
$externalFields = $this->getExternalFields($entityClassName);
$allFields = array_keys($this->propertyNormalizer->normalize(new $entityClassName()));
$this->entityExtraFields = array_diff($allFields, $baseFields, $externalFields);
$this->entityExtraFields[$entityClassName] = array_diff($allFields, $baseFields, $externalFields);
}

return $this->entityExtraFields;
return $this->entityExtraFields[$entityClassName];
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/Bridge/Repository/NormalizerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Symfony\Component\Serializer\SerializerInterface;
use Williarin\WordpressInterop\Criteria\NestedCondition;
use Williarin\WordpressInterop\Criteria\Operand;
use Williarin\WordpressInterop\Criteria\PostRelationshipCondition;
use Williarin\WordpressInterop\Criteria\RelationshipCondition;
use Williarin\WordpressInterop\Criteria\SelectColumns;
use Williarin\WordpressInterop\Criteria\TermRelationshipCondition;
Expand All @@ -26,7 +27,7 @@ public function denormalize(mixed $data, string $type): mixed
]);
}

protected function normalizeCriteria(array $criteria): array
protected function normalizeCriteria(array $criteria, string $entityClassName = null): array
{
$output = [];

Expand All @@ -36,13 +37,14 @@ protected function normalizeCriteria(array $criteria): array
} elseif (
$value instanceof RelationshipCondition
|| $value instanceof TermRelationshipCondition
|| $value instanceof PostRelationshipCondition
|| $value instanceof SelectColumns
) {
$output[] = $value;
} elseif ($value instanceof Operand && $value->isLooseOperator()) {
$output[$field] = $value->getOperand();
} else {
$resolvedValue = $this->validateFieldValue($field, $value);
$resolvedValue = $this->validateFieldValue($field, $value, $entityClassName);
$output[$field] = (string) $this->serializer->normalize($resolvedValue);
}
}
Expand Down
Loading

0 comments on commit a09663d

Please sign in to comment.