Skip to content

Commit

Permalink
Conditions: classes that generate boolean scalar expressions
Browse files Browse the repository at this point in the history
By default these expressions are used in the WHERE clause, but may also be used in HAVING and in JOIN conditions
  • Loading branch information
sad-spirit committed Aug 15, 2023
1 parent 238177c commit 0146df7
Show file tree
Hide file tree
Showing 16 changed files with 960 additions and 0 deletions.
98 changes: 98 additions & 0 deletions src/Condition.php
@@ -0,0 +1,98 @@
<?php

/*
* This file is part of sad_spirit/pg_gateway package
*
* (c) Alexey Borzov <avb@php.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace sad_spirit\pg_gateway;

use sad_spirit\pg_builder\nodes\ScalarExpression;

/**
* Wrapper for ScalarExpression Nodes that (presumably) return boolean values
*
* Conditions are used by fragments that modify WHERE and HAVING clauses and by the JoinFragment
* for an actual JOIN condition
*
* Conditions behave like Specifications from the pattern of the same name and can be combined via
* AND / OR operators. They do not implement isSatisfiedBy() method, though, for more or less obvious reasons.
*/
abstract class Condition implements KeyEquatable, FragmentBuilder
{
/**
* Returns the built fragment
*
* Implementing the FragmentBuilder interface allows directly using the Condition in a list of Fragments
* passed to a Gateway query method. This returns a WhereClauseFragment wrapping around the Condition,
* so it will eventually be appended to the query's WHERE clause.
*
* @return Fragment
*/
public function getFragment(): Fragment
{
return new fragments\WhereClauseFragment($this);
}

/**
* Wrapper method for generateExpression() that clones its return value
*
* The same ScalarExpression instance should not be returned on consecutive calls: it is a feature of the Node
* to keep track of its parent, so it will be removed from one parent if added to the other.
*
* @return ScalarExpression
*/
final public function generateExpression(): ScalarExpression
{
return clone $this->generateExpressionImpl();
}

/**
* Generates the expression that will be added to the Statement
*
* Method name starts with "generate" as a hint: it should preferably generate the ScalarExpression on "as needed"
* basis rather than pre-generate and store that. Real world Conditions will use Parser and parsing may be slow.
*
* @return ScalarExpression
*/
abstract protected function generateExpressionImpl(): ScalarExpression;

/**
* Creates a Condition that combines several other Conditions using AND operator
*
* @param Condition ...$children
* @return conditions\AndCondition
*/
final public static function and(self ...$children): conditions\AndCondition
{
return new conditions\AndCondition(...$children);
}

/**
* Creates a Condition that combines several other Conditions using OR operator
*
* @param Condition ...$children
* @return conditions\OrCondition
*/
final public static function or(self ...$children): conditions\OrCondition
{
return new conditions\OrCondition(...$children);
}

/**
* Creates a negated Condition
*
* @param Condition $child
* @return conditions\NotCondition
*/
final public static function not(self $child): conditions\NotCondition
{
return new conditions\NotCondition($child);
}
}
44 changes: 44 additions & 0 deletions src/conditions/AndCondition.php
@@ -0,0 +1,44 @@
<?php

/*
* This file is part of sad_spirit/pg_gateway package
*
* (c) Alexey Borzov <avb@php.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace sad_spirit\pg_gateway\conditions;

use sad_spirit\pg_gateway\TableLocator;
use sad_spirit\pg_builder\nodes\{
ScalarExpression,
WhereOrHavingClause,
expressions\KeywordConstant
};

/**
* Combines several Conditions using AND operator
*/
final class AndCondition extends LogicalCondition
{
protected function generateExpressionImpl(): ScalarExpression
{
$where = new WhereOrHavingClause();
foreach ($this->children as $child) {
$where->and($child->generateExpression());
}

return $where->condition ?? new KeywordConstant(KeywordConstant::TRUE);
}

public function getKey(): ?string
{
return null !== ($childKeys = $this->getChildKeys())
? 'and.' . TableLocator::hash($childKeys)
: null;
}
}
70 changes: 70 additions & 0 deletions src/conditions/LogicalCondition.php
@@ -0,0 +1,70 @@
<?php

/*
* This file is part of sad_spirit/pg_gateway package
*
* (c) Alexey Borzov <avb@php.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace sad_spirit\pg_gateway\conditions;

use sad_spirit\pg_gateway\{
Condition,
ParameterHolder,
Parametrized,
exceptions\InvalidArgumentException,
holders\ParameterHolderFactory
};

/**
* Base class for Conditions combining several other Conditions using logical operators
*/
abstract class LogicalCondition extends Condition implements Parametrized
{
/** @var Condition[] */
protected array $children = [];

public function __construct(Condition ...$children)
{
if ([] === $children) {
throw new InvalidArgumentException(sprintf(
'%s: at least one child Condition is required',
\get_class($this)
));
}

$this->children = $children;
}

/**
* Returns the array of child Conditions' keys
*
* Note that the array is sorted alphabetically. We do not care in what order Conditions are added:
* https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-EXPRESS-EVAL
* Sorting the keys allows us to have the same key for the same set of Conditions.
*
* @return array|null Returns null if any child Condition returns null from {@see getKey()}
*/
protected function getChildKeys(): ?array
{
$keys = [];
foreach ($this->children as $child) {
if (null === ($key = $child->getKey())) {
return null;
}
$keys[] = $key;
}
\sort($keys, SORT_STRING);
return $keys;
}

public function getParameterHolder(): ?ParameterHolder
{
return ParameterHolderFactory::create(...$this->children);
}
}
63 changes: 63 additions & 0 deletions src/conditions/NotCondition.php
@@ -0,0 +1,63 @@
<?php

/*
* This file is part of sad_spirit/pg_gateway package
*
* (c) Alexey Borzov <avb@php.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace sad_spirit\pg_gateway\conditions;

use sad_spirit\pg_gateway\{
Condition,
ParameterHolder,
Parametrized
};
use sad_spirit\pg_builder\nodes\{
ScalarExpression,
expressions\NegatableExpression,
expressions\NotExpression
};

/**
* Applies NOT operator to the given Condition
*/
final class NotCondition extends Condition implements Parametrized
{
private Condition $child;

public function __construct(Condition $child)
{
$this->child = $child;
}

protected function generateExpressionImpl(): ScalarExpression
{
$childExpression = $this->child->generateExpression();
if ($childExpression instanceof NotExpression) {
return $childExpression->argument;
} elseif ($childExpression instanceof NegatableExpression) {
$childExpression->not = !$childExpression->not;
return $childExpression;
} else {
return new NotExpression($childExpression);
}
}

public function getKey(): ?string
{
return null !== ($key = $this->child->getKey())
? 'not.' . $key
: null;
}

public function getParameterHolder(): ?ParameterHolder
{
return $this->child instanceof Parametrized ? $this->child->getParameterHolder() : null;
}
}
44 changes: 44 additions & 0 deletions src/conditions/OrCondition.php
@@ -0,0 +1,44 @@
<?php

/*
* This file is part of sad_spirit/pg_gateway package
*
* (c) Alexey Borzov <avb@php.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace sad_spirit\pg_gateway\conditions;

use sad_spirit\pg_gateway\TableLocator;
use sad_spirit\pg_builder\nodes\{
ScalarExpression,
WhereOrHavingClause,
expressions\KeywordConstant
};

/**
* Combines several Conditions using OR operator
*/
final class OrCondition extends LogicalCondition
{
protected function generateExpressionImpl(): ScalarExpression
{
$where = new WhereOrHavingClause();
foreach ($this->children as $child) {
$where->or($child->generateExpression());
}

return $where->condition ?? new KeywordConstant(KeywordConstant::TRUE);
}

public function getKey(): ?string
{
return null !== ($childKeys = $this->getChildKeys())
? 'or.' . TableLocator::hash($childKeys)
: null;
}
}
52 changes: 52 additions & 0 deletions src/conditions/ParametrizedCondition.php
@@ -0,0 +1,52 @@
<?php

/*
* This file is part of sad_spirit/pg_gateway package
*
* (c) Alexey Borzov <avb@php.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace sad_spirit\pg_gateway\conditions;

use sad_spirit\pg_gateway\{
Condition,
ParameterHolder,
Parametrized,
holders\SimpleParameterHolder
};
use sad_spirit\pg_builder\nodes\ScalarExpression;

/**
* A decorator around Condition that keeps values of parameters used by that Condition
*/
final class ParametrizedCondition extends Condition implements Parametrized
{
private Condition $wrapped;
private array $parameters;

public function __construct(Condition $wrapped, array $parameters)
{
$this->wrapped = $wrapped;
$this->parameters = $parameters;
}

protected function generateExpressionImpl(): ScalarExpression
{
return $this->wrapped->generateExpressionImpl();
}

public function getKey(): ?string
{
return $this->wrapped->getKey();
}

public function getParameterHolder(): ?ParameterHolder
{
return new SimpleParameterHolder($this->wrapped, $this->parameters);
}
}

0 comments on commit 0146df7

Please sign in to comment.