Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions docs/clauses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
id: clauses
title: Clauses
slug: /clauses
---

Clauses are Access' way to store query conditions, order by information and
filters. These clauses can be converted to SQL, be used to filter manipulate
collections.

There are three types of clauses: conditions, filters, and orders.

## Condition clauses

Condition clauses can be used to add `WHERE` conditions to a query, there are a
bunch of them builtin. A lot of these clauses will speak for themselves.

- `Access\Clauses\Condition\Equals`
- `Access\Clauses\Condition\GreaterThan`
- `Access\Clauses\Condition\GreaterThanOrEquals`
- `Access\Clauses\Condition\IsNotNull`
- `Access\Clauses\Condition\IsNull`
- `Access\Clauses\Condition\LessThen`
- `Access\Clauses\Condition\LessThanOrEquals`
- `Access\Clauses\Condition\NotEquals`
- `Access\Clauses\Condition\In`
- `Access\Clauses\Condition\NotIn`

The condition clauses can be used in code and in SQL.

```php
// in query form
$query = new Select(...);
$query->where(Equals('id', 1));

// SELECT * FROM ... WHERE id = 1
```

```php
// in code form
$users = $userRepo->findAllCollection(...)
$users->applyClause(Equals('id', 1));
```

The code form will still fetch all the users from the database, so this form is
mostly useful you want to select a couple of records from the collection, but
also want to keep the original collection around. Pre-fetching a bunch of
records to process later to prevent `n+1` queries is a great example of this.
This is how the clauses are used in the [presenters](presenters).

## Ordering clauses

To order the projects you can use the `Access\Clause\OrderBy` clauses. There
are only three order by clauses:

- `Access\Clause\OrderBy\Ascending`
- `Access\Clause\OrderBy\Descending`
- `Access\Clause\OrderBy\Random`

The condition clauses can be used in code and in SQL.

```php
// in query form
$query = new Select(...);
$query->orderBy(Ascending('id'));

// SELECT * FROM ... ORDER BY id ASC
```

```php
// in code form
$users = $userRepo->findAllCollection(...)
$users->applyClause(Ascending('id'));
```

## Filtering clauses

Currently there is a single filter clauses, and it only has a code form, it
can't be used in a query.

- `Access\Clause\Filter\Unique`: Will filter out entities with a duplicate
field value

## Multiple clauses

And if you want to mix multiple clauses together, if, for example, you want the
list to contain published projects and also want to order them. The special
`Access\Clauses\Multiple` clause will help you here, it combines multiple
conditions together and all conditions need to be true. Or if you provide
multiple order by clauses they all will be used (you can sorting on status and
then on name, for example). There is also the `Access\Clauses\MultipleOr` clause
if you want only one of the condition clauses to be true. You can add as many
clauses to the `Multiple` as you like, and you can mix order by and condition
clauses.

```php
// in query form
$query = new Select(...);
$query->where(new Multiple(
Equals('id', 1),
Equals('name', 'Dave'),
));

// SELECT * FROM ... WHERE id = 1 AND name = "Dave"
```

When applying `Multiple` clauses to a collection, conditions and orders can be
mixed. This is how presenter can use a "single" clause parameter for multiple
purposes.

```php
$users = $userRepo->findAllCollection(...)
$users->applyClause(new Multiple(
Equals('name', 'Dave'),
Ascending('id'),
));
```

### Empty multiple clauses

When a `Multiple` clause is empty there is a bit of special handling. When used
in the context of a condition, it will _not_ match any entities and `1 = 2` is
used in a query. This is to prevent accidental overfetching, when building
`Multiple` clauses programmatically. When there are mixed clauses inside the
`Multiple` clause, if it contains a single condition it is considered a
condition clause, or if it's completely empty (no "type" can be determined).

When used in the context of ordering, it will just be ignored.
2 changes: 2 additions & 0 deletions docs/presenters.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ class UserWithOrderedPublishedProjectsPresenter extends EntityPresenter
}
```

More information about [clauses can be found here](clauses).

## `Presenter` instance

### Dependency injection
Expand Down
7 changes: 7 additions & 0 deletions src/Clause/ClauseInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,11 @@
*/
interface ClauseInterface
{
/**
* Is another clause equal to this one
*
* @param ClauseInterface $clause Clause to compare with
* @return bool Are the clauses equal
*/
public function equals(ClauseInterface $clause): bool;
}
54 changes: 52 additions & 2 deletions src/Clause/Condition/Condition.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@

namespace Access\Clause\Condition;

use Access\Clause\ClauseInterface;
use Access\Clause\ConditionInterface;
use Access\Clause\Field;
use Access\Collection;
use Access\Database;
use Access\Entity;
use Access\Exception;
use Access\Query;
use Access\Query\QueryGeneratorState;
use Access\Query\QueryGeneratorStateContext;
use Access\Query\Select;

/**
Expand Down Expand Up @@ -95,6 +98,45 @@ public function getField(): Field
return $this->field;
}

/**
* Is this condition equal to another clause
*
* @param ClauseInterface $clause Clause to compare with
* @return bool Are the clauses equal
*/
public function equals(ClauseInterface $clause): bool
{
if ($this::class !== $clause::class) {
return false;
}

/** @var static $clause */

$driver = Database::getDriverOrDefault(null);

$stateOne = new QueryGeneratorState(
$driver,
QueryGeneratorStateContext::Condition,
'a',
'b',
);

$sqlOne = $this->getConditionSql($stateOne);
$this->injectConditionValues($stateOne);

$stateTwo = new QueryGeneratorState(
$driver,
QueryGeneratorStateContext::Condition,
'a',
'b',
);

$sqlTwo = $clause->getConditionSql($stateTwo);
$clause->injectConditionValues($stateTwo);

return $sqlOne === $sqlTwo && $stateOne->equals($stateTwo);
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -242,7 +284,11 @@ public function getConditionSql(QueryGeneratorState $state): string
],
$condition,
);
} elseif (is_array($this->value) || $this->value instanceof Collection) {
} elseif (
is_array($this->value) ||
$this->value instanceof Collection ||
$this->value instanceof \ArrayIterator
) {
if (count($this->value) > 0) {
$condition = str_replace(
'?',
Expand Down Expand Up @@ -274,7 +320,11 @@ public function injectConditionValues(QueryGeneratorState $state): void
$state->addSubQueryValues($this->value);
} elseif ($this->value === null) {
// sql is converted to `IS NULL`
} elseif (is_array($this->value) || $this->value instanceof Collection) {
} elseif (
is_array($this->value) ||
$this->value instanceof Collection ||
$this->value instanceof \ArrayIterator
) {
$values = Query::toDatabaseFormat($this->value);

// empty list will result in no emitted values, this links up with
Expand Down
18 changes: 18 additions & 0 deletions src/Clause/Filter/Unique.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace Access\Clause\Filter;

use Access\Clause\ClauseInterface;
use Access\Clause\Filter\Filter;
use Access\Entity;

Expand All @@ -30,6 +31,23 @@ public function __construct(string $fieldName)
$this->fieldName = $fieldName;
}

/**
* Is this filter equal to another clause
*
* @param ClauseInterface $clause Clause to compare with
* @return bool Are the clauses equal
*/
public function equals(ClauseInterface $clause): bool
{
if ($this::class !== $clause::class) {
return false;
}

/** @var static $clause */

return $this->fieldName === $clause->fieldName;
}

/**
* Create the finder function for this filter clause
*
Expand Down
89 changes: 83 additions & 6 deletions src/Clause/Multiple.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
use Access\Clause\ClauseInterface;
use Access\Clause\ConditionInterface;
use Access\Collection;
use Access\Database;
use Access\Entity;
use Access\Query\QueryGeneratorState;
use Access\Query\QueryGeneratorStateContext;

/**
* Multiple clauses to mixed and/or match
Expand Down Expand Up @@ -53,6 +55,46 @@ public function __construct(ClauseInterface ...$clauses)
$this->clauses = $clauses;
}

/**
* Is this multiple clause equal to another clause
*
* @param ClauseInterface $clause Clause to compare with
* @return bool Are the clauses equal
*/
public function equals(ClauseInterface $clause): bool
{
if ($this::class !== $clause::class) {
return false;
}

/** @var static $clause */

// dummy driver to generate the SQL
$driver = Database::getDriverOrDefault(null);

$stateOne = new QueryGeneratorState(
$driver,
QueryGeneratorStateContext::Condition,
'a',
'b',
);

$sqlOne = $this->getConditionSql($stateOne);
$this->injectConditionValues($stateOne);

$stateTwo = new QueryGeneratorState(
$driver,
QueryGeneratorStateContext::Condition,
'a',
'b',
);

$sqlTwo = $clause->getConditionSql($stateTwo);
$clause->injectConditionValues($stateTwo);

return $sqlOne === $sqlTwo && $stateOne->equals($stateTwo);
}

/**
* Return the number of clauses currently available
*/
Expand Down Expand Up @@ -207,17 +249,44 @@ protected function getMultipleSql(string $combineWith, QueryGeneratorState $stat
{
$conditionParts = [];

// without any clauses, we can't determine if this is a multiple condition.
// to be on the safe side, we assume it is a multiple condition
$isMultipleCondition = count($this->clauses) === 0;

foreach ($this->clauses as $clause) {
if ($clause instanceof ConditionInterface) {
if (
$state->getContext() === QueryGeneratorStateContext::Condition &&
$clause instanceof ConditionInterface
) {
$isMultipleCondition = true;
$conditionParts[] = $clause->getConditionSql($state);
} elseif (
$state->getContext() === QueryGeneratorStateContext::OrderBy &&
$clause instanceof OrderByInterface
) {
$conditionParts[] = $clause->getConditionSql($state);
}
}

if (empty($conditionParts)) {
// empty conditions make no sense...
// droppping the whole condition is risky because you may
// over-select a whole bunch of records, better is to under-select.
return '1 = 2';
if ($state->getContext()->allowEmptyMultiple()) {
return '';
}

if ($isMultipleCondition) {
// empty conditions make no sense...
// droppping the whole condition is risky because you may
// over-select a whole bunch of records, better is to under-select.
return '1 = 2';
}

return '';
}

if ($state->getContext() === QueryGeneratorStateContext::OrderBy) {
// we are in the order context, just combine them with a comma,
// without wrapping them in parentheses.
return implode(', ', $conditionParts);
}

$combinedConditions = implode($combineWith, $conditionParts);
Expand All @@ -237,7 +306,15 @@ protected function getMultipleSql(string $combineWith, QueryGeneratorState $stat
public function injectConditionValues(QueryGeneratorState $state): void
{
foreach ($this->clauses as $clause) {
if ($clause instanceof ConditionInterface) {
if (
$state->getContext() === QueryGeneratorStateContext::Condition &&
$clause instanceof ConditionInterface
) {
$clause->injectConditionValues($state);
} elseif (
$state->getContext() === QueryGeneratorStateContext::OrderBy &&
$clause instanceof OrderByInterface
) {
$clause->injectConditionValues($state);
}
}
Expand Down
Loading