Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collection function refactorings #587

Merged
merged 7 commits into from
Feb 12, 2023
Merged
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
2 changes: 0 additions & 2 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/collection-filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Orm brings these prepared aggregation functions:
- MinAggregateFunction
- MaxAggregateFunction

All those functions are implemented both for Dbal and Array collections and they are registered in a repository as commonly provided collection functions.
All those functions are implemented both for Dbal and Array collections, and they are registered in a repository as commonly provided collection functions.

To use a collection function, pass the function name and then its arguments –- all aggregation functions take only one argument – an expression that should be aggregated. Let’s see an example:

Expand Down
16 changes: 8 additions & 8 deletions docs/collection-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

Collection functions are a powerful extension point that will allow you to write a custom filtering or an ordering behavior.

The custom filtering or ordering requires its own implementation for each storage implementation; ideally you write it for both Dbal & Array to allow use of persisted and unpersisted collections. Both of the Dbal & Array implementations bring their own interface you have to implement directly.
The custom filtering or ordering requires its own implementation for each storage implementation; ideally you write it for both Dbal & Array to allow use of persisted and un-persisted collections. Both of the Dbal & Array implementations bring their own interface you have to implement directly.

<div class="note">

**Why do we have ArrayCollection and DbalCollection?**

Collection itself is independent from storage implementation. It is your choice if your collection function will work in both cases - for `ArrayCollection` and `DbalCollection`. Let us remind you, `ArrayCollection`s are commonly used in relationships when you set new entities into the relationship but until the relationship is persisted, you will work with an `ArrayCollection`.
Collection itself is independent of storage implementation. It is your choice if your collection function will work in both cases - for `ArrayCollection` and `DbalCollection`. Let us remind you, `ArrayCollection`s are commonly used in relationships when you set new entities into the relationship but until the relationship is persisted, you will work with an `ArrayCollection`.
</div>

Collection functions can be used in `ICollection::findBy()` or `ICollection::orderBy()` methods. A collection function is used as an array argument, the first value is the function identifier (it is recommended using function's class name) and then function's arguments as other array values. Collection functions may be used together, also nest together, so you can reuse them.
Expand Down Expand Up @@ -49,7 +49,7 @@ class UsersRepository extends Nextras\Orm\Repository\Repository

Dbal collection functions have to implement `Nextras\Orm\Collection\Functions\IQueryBuilderFunction` interface. The only required method takes three arguments: `DbalQueryBuilderHelper` for easier user input processing, `QueryBuilder` for creating table joins, and a user input/function parameters.

Collection function has to return a `DbalExpressionResult` object. This object holds parts of SQL query which may be processed by Netras Dbal's SqlProcessor. Because you are not adding SQL parts directly to QueryBuilder but rather return them in `DbalExpressionResult`, you may compose multiple collection functions together.
Collection function has to return a `DbalExpressionResult` object. This object holds parts of SQL query which may be processed by Nextras Dbal's SqlProcessor. Because you are not adding SQL parts directly to QueryBuilder but rather return them in `DbalExpressionResult`, you may compose multiple collection functions together.

Let's see an example: a "Like" collection function; We want to compare a property (expression) via SQL's LIKE operator with a prefix comparison.

Expand All @@ -59,11 +59,11 @@ $users->findBy(
);
```

In the example we would like to use the custom `LikeFunction` to filter users by their phones that start with `+420` prefix. Our function will implement `IQueryBuilderFunction` interface and will receive `$args` with `phone` and `+420`. The column/property argument may be quite dynamic too. What if the user passes `address->zipcode` (i.e. a relationship expression) instead of a simple `phone`, such expression would require table joins; doing it all by hand would be difficult. Therefore Orm provides `DbalQueryBuilderHelper` that will handle all this for you. Use `processPropertyExpr` method to obtain a `DbalExpressionResult` for the column/property argument. Then just append needed SQL to the returned expression, e.g. LIKE operator with a Dbal's argument. That's all!
In the example we would like to use the custom `LikeFunction` to filter users by their phones that start with `+420` prefix. Our function will implement `IQueryBuilderFunction` interface and will receive `$args` with `phone` and `+420`. The column/property argument may be quite dynamic too. What if the user passes `address->zipcode` (i.e. a relationship expression) instead of a simple `phone`, such expression would require table joins; doing it all by hand would be difficult. Therefore, Orm provides `DbalQueryBuilderHelper` that will handle all this for you. Use `processPropertyExpr` method to obtain a `DbalExpressionResult` for the column/property argument. Then just append needed SQL to the returned expression, e.g. LIKE operator with a Dbal's argument. That's all!

```php
use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\Helpers\DbalExpressionResult;
use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult;
use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper;

final class LikeFunction implements IQueryBuilderFunction
Expand All @@ -85,7 +85,7 @@ final class LikeFunction implements IQueryBuilderFunction

The helper processed value may not be just a column, but also another expression returned from another collection function.

If you need more advance operation then appending the expression, construct a new `DbalExpressionResult` object. Use Dbal's `%ex` modifier to expand the already processed expression. Some properties of the original expression result may be lost by creating a new expression result instance; if needed, pass the original's values as additional constructor parameters.
If you need more advance operation than appending the expression, construct a new `DbalExpressionResult` object. Use Dbal's `%ex` modifier to expand the already processed expression. Some properties of the original expression result may be lost by creating a new expression result instance; if needed, pass the original's values as additional constructor parameters.

```php
$expression = $helper->processPropertyExpr($builder, $args[0]);
Expand All @@ -96,7 +96,7 @@ return new DbalExpressionResult(['SUBSTRING(%ex, 0, %i) = %s', $expression->args

Array collection functions have to implement `Netras\Orm\Collection\Functions\IArrayFunction` interface. It is different to Dbal's interface, because the filtering happens directly in PHP now. The only required method takes `ArrayCollectionHelper` for easier entity property processing, `IEntity` entity to check if it should (not) be filtered out, and user input/function parameters.

Array collection function returns a mixed value, the kind depends in which context it will be evaluated. The value will be interpreted as boolean in the filtering context to indicate if the entity should be filtered out; the value will be used for comparison of two entities in the ordering context (using the spaceship operator).
Array collection function returns a mixed value, the kind depends on which context it will be evaluated. The value will be interpreted as boolean in the filtering context to indicate if the entity should be filtered out; the value will be used for comparison of two entities in the ordering context (using the spaceship operator).

Let's see an example: a "Like" collection function; We want to compare any (property) expression to passed user-input value with a prefix comparison.

Expand All @@ -122,5 +122,5 @@ Our function implements the `IArrayFunction` interface and will receive helper o

<div class="note">

PostgreSQL is case-sensitive, so you should apply the lower function & a functional index; These modifications are case-specific, therefore the LIKE functionality is not provided in Orm by default.
Postgres is case-sensitive, so you should apply the lower function & a functional index; These modifications are case-specific, therefore the LIKE functionality is not provided in Orm by default.
</div>
2 changes: 1 addition & 1 deletion docs/collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Read more in the [collection filtering chapter](collection-filtering).

#### Sorting

You can easily sort the collection by an `orderBy()` method; The `orderBy()` method accepts a property name and a sorting direction. By default, values are sorted in an ascending order.
You can easily sort the collection by an `orderBy()` method; The `orderBy()` method accepts a property name and a sorting direction. By default, values are sorted in ascending order.

To change the order, use `ICollection::ASC` or `ICollection::DESC` constants. If the sorting property (or property expression) may contain a null value, use more specific sorting constants: `ICollection::ASC_NULLS_LAST`, `ICollection::ASC_NULLS_FIRST`, `ICollection::DESC_NULLS_LAST`, or `ICollection::DESC_NULLS_FIRST`.

Expand Down
4 changes: 2 additions & 2 deletions docs/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class EventsMapper extends Mapper

Conventions take care about converting column names to property names. Dbal mapper's conventions are represented by interface `Nextras\Orm\Mapper\Dbal\Conventions\IConventions` interface.

Orm comes with two predefined inflectors, that modify the basic conventions behavior:
Orm comes with two predefined inflectors, that modify the basic conventions' behavior:
- CamelCaseInflector
- SnakeCaseInflector

Expand Down Expand Up @@ -59,7 +59,7 @@ class EventsMapper extends Mapper

#### Properties' converters

Conventions offers an API for data transformation when the data are passed from storage to PHP and otherwise. The aforementioned `setMapping($entityName, $storageName, $toEntityCb, $toStorageCb)` method has two optional parameters that accept callbacks. These callbacks receive the value and key parameters and must return the new converted value. The first callback is for conversion from the storage to PHP, the second is for conversion from PHP to the storage. Let's see an example:
Conventions offer an API for data transformation when the data are passed from storage to PHP and otherwise. The aforementioned `setMapping($entityName, $storageName, $toEntityCb, $toStorageCb)` method has two optional parameters that accept callbacks. These callbacks receive the value and key parameters and must return the new converted value. The first callback is for conversion from the storage to PHP, the second is for conversion from PHP to the storage. Let's see an example:

```php
/**
Expand Down
2 changes: 1 addition & 1 deletion docs/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ $orm->books->persistAndFlush($book);

Calling `persistAndFlush()` on `$book` recursively persists the author, the publisher and the book entity and encloses the whole operation in a (database) transaction.

Nextras Orm provides a mechanism to use a constant number of queries: it does not matter, how much data you will fetch and ouput or how many inner cycles you will use. Orm will fetch all needed data efficiently in advance. Let's see an example:
Nextras Orm provides a mechanism to use a constant number of queries: it does not matter, how much data you will fetch and output or how many inner cycles you will use. Orm will fetch all needed data efficiently in advance. Let's see an example:

```php
$authors = $orm->authors->findAll();
Expand Down
2 changes: 1 addition & 1 deletion docs/mapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Collection results form Array mapper are returned as an `ArrayCollection` instan

Dbal mapper uses [Nextras Dbal][1] library. Both Nextras Dbal and Orm support the following engines:
- MySQL,
- PostgreSQL,
- Postgres,
- SQL Server (currently not supported auto-update mapping).

Dbal mapper is aliased as `Nextras\Orm\Mapper\Mapper` class. To set mapper's database **table name** set `$tableName` property or override `getTableName()` method.
Expand Down
2 changes: 1 addition & 1 deletion docs/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ assert($book->title === 'Test');

#### Clear

Batch processing is often memory demanding. To free some memory, you may use `IModel::clear()` method. Calling clear will clear all caches and references to all fetched entities, it also nulls their values & data. Be aware that you should never access these entity after calling the clear method. Also, be careful not to store any references to this entities.
Batch processing is often memory demanding. To free some memory, you may use `IModel::clear()` method. Calling clear will clear all caches and references to all fetched entities, it also nulls their values & data. Be aware that you should never access these entity after calling the clear method. Also, be careful not to store any references to these entities.

```php
$lastId = 0;
Expand Down
2 changes: 1 addition & 1 deletion docs/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ All relationships can have defined a cascade behavior. Cascade behavior defines
{relModifier EntityName::$reversePropertyName, cascade=[]}
```

The persist and remove methods process entity with its cascade. You can turn off by the second optional method argument:
The `persist()` and `remove()` methods process entity with its cascade. You can turn off by the second optional method argument:

```php
$usersRepository->persist($user, false);
Expand Down
4 changes: 2 additions & 2 deletions docs/repository.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ final class BooksMapper extends Mapper

#### Identity map

Repository uses Identity Map pattern. Therefore only one instance of Entity can exist in your runtime. Selecting the same entity by another query will still return the same entity, even when entity changes were not persisted.
Repository uses Identity Map pattern. Therefore, only one instance of Entity can exist in your runtime. Selecting the same entity by another query will still return the same entity, even when entity changes were not persisted.

```php
// in this example title property is unique
Expand All @@ -79,7 +79,7 @@ $book1 === $book2; // true

#### Persisting

To save your changes, you have to explicitly persist the changes by calling `IModel::persist()` method, no matter if you are creating or updating the entity. By default, the repository will persist all other connected entities with persist cascade. Also, Orm will take care of needed persistence ordering.
To save your changes, you have to explicitly persist the changes by calling `IModel::persist()` method, no matter if you are creating or updating the entity. By default, the repository will persist all others connected entities with persist cascade. Also, Orm will take care of needed persistence ordering.

Persistence is run in a transaction. Calling `persist()` automatically starts a transaction if it was not started earlier. The transaction is committed by calling `IModel::flush()` method. You can persist and flush changes at once by using `IModel::persistAndFlush()` method. Persisting automatically attaches the entity to the repository, if it has not been attached earlier.

Expand Down
31 changes: 14 additions & 17 deletions src/Collection/Aggregations/AnyAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@


use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\Helpers\DbalExpressionResult;
use Nextras\Orm\Collection\Helpers\DbalJoinEntry;
use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult;
use Nextras\Orm\Collection\Functions\Result\DbalTableJoin;
use Nextras\Orm\Exception\InvalidStateException;
use function array_merge;
use function array_pop;
Expand Down Expand Up @@ -60,26 +60,23 @@ public function aggregateExpression(
throw new InvalidStateException('Aggregation applied over expression without a relationship');
}

$joins[] = new DbalJoinEntry(
$join->toExpression,
$join->toArgs,
$join->toAlias,
"($join->onExpression) AND $joinExpression",
array_merge($join->onArgs, $joinArgs),
$join->conventions
$joins[] = new DbalTableJoin(
toExpression: $join->toExpression,
toArgs: $join->toArgs,
toAlias: $join->toAlias,
onExpression: "($join->onExpression) AND $joinExpression",
onArgs: array_merge($join->onArgs, $joinArgs),
conventions: $join->conventions,
);

$primaryKey = $join->conventions->getStoragePrimaryKey()[0];

return new DbalExpressionResult(
'COUNT(%table.%column) > 0',
[$join->toAlias, $primaryKey],
$joins,
$expression->groupBy,
null,
true,
null,
null
expression: 'COUNT(%table.%column) > 0',
args: [$join->toAlias, $primaryKey],
joins: $joins,
groupBy: $expression->groupBy,
isHavingClause: true,
);
}
}
31 changes: 14 additions & 17 deletions src/Collection/Aggregations/CountAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@


use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\Helpers\DbalExpressionResult;
use Nextras\Orm\Collection\Helpers\DbalJoinEntry;
use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult;
use Nextras\Orm\Collection\Functions\Result\DbalTableJoin;
use Nextras\Orm\Exception\InvalidStateException;


Expand Down Expand Up @@ -66,26 +66,23 @@ public function aggregateExpression(
throw new InvalidStateException('Aggregation applied over expression without a relationship');
}

$joins[] = new DbalJoinEntry(
$join->toExpression,
$join->toArgs,
$join->toAlias,
"($join->onExpression) AND $joinExpression",
array_merge($join->onArgs, $joinArgs),
$join->conventions
$joins[] = new DbalTableJoin(
toExpression: $join->toExpression,
toArgs: $join->toArgs,
toAlias: $join->toAlias,
onExpression: "($join->onExpression) AND $joinExpression",
onArgs: array_merge($join->onArgs, $joinArgs),
conventions: $join->conventions,
);

$primaryKey = $join->conventions->getStoragePrimaryKey()[0];

return new DbalExpressionResult(
'COUNT(%table.%column) >= %i AND COUNT(%table.%column) <= %i',
[$join->toAlias, $primaryKey, $this->atLeast, $join->toAlias, $primaryKey, $this->atMost],
$joins,
$expression->groupBy,
null,
true,
null,
null
expression: 'COUNT(%table.%column) >= %i AND COUNT(%table.%column) <= %i',
args: [$join->toAlias, $primaryKey, $this->atLeast, $join->toAlias, $primaryKey, $this->atMost],
joins: $joins,
groupBy: $expression->groupBy,
isHavingClause: true,
);
}
}
2 changes: 1 addition & 1 deletion src/Collection/Aggregations/IArrayAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface IArrayAggregator extends IAggregator
{
/**
* @param array<T> $values
* @return T
* @return T|null
*/
function aggregateValues(array $values);
}
2 changes: 1 addition & 1 deletion src/Collection/Aggregations/IDbalAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\Helpers\DbalExpressionResult;
use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult;


interface IDbalAggregator extends IAggregator
Expand Down
Loading