Skip to content

Feature/Add Eloquent criteria parser. #1

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

Merged
merged 1 commit into from
Mar 23, 2024
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: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

strategy:
matrix:
php-version: [ '8.1 ' ]
php-version: [ '8.1', '8.2', '8.3' ]

steps:
- name: Checkout source code
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ composer.phar
/vendor/
.idea/
/infrastructure/
/coverage/
*[N|n]o[G|g]it*
coverage/
coverage.xml
test.xml
.phpunit.cache/
.phpunit.result.cache
composer.lock
28 changes: 16 additions & 12 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,39 @@
"authors": [
{
"name": "Unay Santisteban",
"email": "usantisteban@othercode.es"
"email": "usantisteban@othercode.io"
}
],
"minimum-stability": "stable",
"require": {
"php": "^8.1.0",
"complex-heart/domain-model": "^2.0.0",
"complex-heart/criteria": "^2.0.0",
"laravel/framework": "^9.0.0"
"complex-heart/sdk": "^1.0.0",
"laravel/framework": "^10.0.0"
},
"require-dev": {
"pestphp/pest": "^1.22.3",
"pestphp/pest-plugin-mock": "^1.0.0",
"pestphp/pest-plugin-faker": "^1.0.0",
"phpstan/phpstan": "^1.9.0"
"pestphp/pest": "*",
"pestphp/pest-plugin-faker": "^2.0",
"phpstan/phpstan": "*",
"mockery/mockery": "^1.6"
},
"autoload": {
"psr-4": {
"ComplexHeart\\OnLaravel\\": "src/"
"ComplexHeart\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"ComplexHeart\\Test\\OnLaravel\\": "tests/"
"ComplexHeart\\Tests\\": "tests/"
}
},
"scripts": {
"test": "vendor/bin/pest --configuration=phpunit.xml --coverage-clover=coverage.xml --log-junit=test.xml --coverage-html=coverage",
"analyse": "vendor/bin/phpstan analyse src --no-progress --level=5"
"test": "vendor/bin/pest --configuration=phpunit.xml --coverage-clover=coverage.xml --log-junit=test.xml",
"test-cov": "vendor/bin/pest --configuration=phpunit.xml --coverage-html=coverage",
"analyse": "vendor/bin/phpstan analyse src --no-progress --level=8",
"check": [
"@analyse",
"@test"
]
},
"config": {
"allow-plugins": {
Expand Down
36 changes: 17 additions & 19 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
colors="true"
verbose="true"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
<report>
<clover outputFile="./coverage.xml"/>
</report>
</coverage>
<testsuites>
<testsuite name="unit">
<directory>./tests</directory>
</testsuite>
</testsuites>
<logging/>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" cacheDirectory=".phpunit.cache">
<coverage>
<report>
<clover outputFile="./coverage.xml"/>
</report>
</coverage>
<testsuites>
<testsuite name="unit">
<directory>./tests</directory>
</testsuite>
</testsuites>
<logging/>
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
sonar.projectName=Complex Hear on Laravel
sonar.projectName=Complex Heart on Laravel
sonar.projectKey=ComplexHeart_on-laravel
sonar.organization=complexheart
sonar.language=php
Expand Down
178 changes: 178 additions & 0 deletions src/Infrastructure/Laravel/Persistence/BasicCriteriaParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<?php

declare(strict_types=1);

namespace ComplexHeart\Infrastructure\Laravel\Persistence;

use ComplexHeart\Domain\Criteria\Criteria;
use ComplexHeart\Domain\Criteria\Filter;
use ComplexHeart\Domain\Criteria\FilterGroup;
use ComplexHeart\Domain\Criteria\Operator;
use ComplexHeart\Domain\Criteria\Order;
use ComplexHeart\Domain\Criteria\Page;
use ComplexHeart\Infrastructure\Laravel\Persistence\Contracts\EloquentCriteriaParser;
use Illuminate\Contracts\Database\Query\Builder;

/**
* Class DefaultCriteriaParser
*
* @author Unay Santisteban <usantisteban@othercode.io>
* @package ComplexHeart\Infrastructure\Laravel\Persistence
*/
class BasicCriteriaParser implements EloquentCriteriaParser
{
/**
* DefaultCriteriaParser constructor.
*
* @param array<string, string> $filterAttributes
*/
public function __construct(private readonly array $filterAttributes = [])
{
}

/**
* Returns the persistence attribute name based on given
* domain attribute name if exists.
*
* The key is the domain name attribute nad the value is
* the name of the attribute in the persistence system.
*
* 'domain-attribute' => 'persistence-attribute'
*
* For example:
*
* $this->filterAttributes = [
* 'owner' => 'user_id',
* 'title' => 'title',
* 'type' => 'type',
* 'opensource' => 'is_opensource',
* ]
*
* $this->filterAttribute('owner') returns 'user_id'
*
* @param string $fieldAttribute
* @return string
*/
private function filterAttribute(string $fieldAttribute): string
{
return $this->filterAttributes[$fieldAttribute] ?? $fieldAttribute;
}

/**
* Apply a criteria into the given Query Builder.
*
* @param Builder $builder
* @param Criteria $criteria
* @return Builder
*/
public function applyCriteria(Builder $builder, Criteria $criteria): Builder
{
$builder = $this->applyFilterGroups($builder, $criteria->groups());
$builder = $this->applyOrdering($builder, $criteria->order());

return $this->applyPage($builder, $criteria->page());
}

/**
* Apply the given list of filter groups into the given QueryBuilder.
*
* @param Builder $builder
* @param array<int, FilterGroup> $groups
* @return Builder
*/
private function applyFilterGroups(Builder $builder, array $groups): Builder
{
foreach ($groups as $index => $group) {
$builder = $index === 0
? $this->applyFilters($builder, $group)
: $builder->orWhere(function (Builder $query) use ($group) {
$this->applyFilters($query, $group);
});
}

return $builder;
}

/**
* Apply a set of filters into the given QueryBuilder.
*
* @param Builder $builder
* @param FilterGroup<Filter> $filters
* @return Builder
*/
private function applyFilters(Builder $builder, FilterGroup $filters): Builder
{
foreach ($filters as $filter) {
$builder = $this->applyFilter($builder, $filter);
}

return $builder;
}

/**
* Apply a filter into the given QueryBuilder.
*
* @param Builder $builder
* @param Filter $filter
* @return Builder
*/
private function applyFilter(Builder $builder, Filter $filter): Builder
{
$field = $this->filterAttribute($filter->field());

switch ($filter->operator()) {
case Operator::IN:
$builder->whereIn($field, $filter->value());
break;
case Operator::NOT_IN:
$builder->whereNotIn($field, $filter->value());
break;
// redirect the contains operator to like.
case Operator::CONTAINS:
$builder->where($field, Operator::LIKE->value, $filter->value());
break;
// redirect the not contains operator to not like.
case Operator::NOT_CONTAINS:
$builder->where($field, Operator::NOT_LIKE->value, $filter->value());
break;
default:
$builder->where($field, $filter->operator()->value, $filter->value());
}

return $builder;
}

/**
* Apply the ordering settings into the given QueryBuilder.
*
* @param Builder $builder
* @param Order $ordering
* @return Builder
*/
private function applyOrdering(Builder $builder, Order $ordering): Builder
{
if (!$ordering->isNone()) {
$filterAttribute = $this->filterAttribute($ordering->by());

$builder = ($ordering->isRandom())
? $builder->inRandomOrder()
: $builder->orderBy($filterAttribute, $ordering->type()->value);
}

return $builder;
}

/**
* Apply the page settings (limit and offset) into the given QueryBuilder.
*
* @param Builder $builder
* @param Page $page
* @return Builder
*/
private function applyPage(Builder $builder, Page $page): Builder
{
$builder = $builder->limit($page->limit());

return $builder->offset($page->offset());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace ComplexHeart\Infrastructure\Laravel\Persistence\Contracts;

use ComplexHeart\Domain\Criteria\Criteria;
use Illuminate\Contracts\Database\Query\Builder;

/**
* Interface EloquentCriteriaParser
*
* @author Unay Santisteban <usantisteban@othercode.io>
* @package ComplexHeart\Infrastructure\Laravel\Persistence\Contracts
*/
interface EloquentCriteriaParser
{
/**
* Apply a criteria into the given QueryBuilder.
*
* @param Builder $builder
* @param Criteria $criteria
* @return Builder
*/
public function applyCriteria(Builder $builder, Criteria $criteria): Builder;
}
48 changes: 48 additions & 0 deletions tests/CreatesApplication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace ComplexHeart\Tests;

use ComplexHeart\Tests\Fixtures\Infrastructure\Persistence\Laravel\Migrations\CreateUsersTable;
use Illuminate\Database\Capsule\Manager as Capsule;

/**
* Trait CreatesApplication
*
* @author Unay Santisteban <usantisteban@othercode.io>
* @package ComplexHeart\Tests
*/
trait CreatesApplication
{
private function bootEloquent(): void
{
$capsule = new Capsule();
$capsule->addConnection([
'driver' => 'sqlite',
'database' => ':memory:',
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();

$migrations = [
CreateUsersTable::class,
];

foreach ($migrations as $migration) {
(new $migration($capsule->schema()))->up();
}
}

/**
* Currently this method only boots the database and
* eloquent system. In future iterations this may really
* boot a complete Laravel application instance.
*
* @return void
*/
public function createApplication(): void
{
$this->bootEloquent();
}
}
24 changes: 24 additions & 0 deletions tests/Fixtures/Domain/Contracts/UserRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace ComplexHeart\Tests\Fixtures\Domain\Contracts;

use ComplexHeart\Domain\Criteria\Criteria;
use ComplexHeart\Tests\Fixtures\Domain\User;
use Illuminate\Support\Collection;

/**
* Interface UserRepository
*
* @author Unay Santisteban <usantisteban@othercode.io>
* @package ComplexHeart\Tests\Fixtures\Domain\Contracts
*/
interface UserRepository
{
/**
* @param Criteria $criteria
* @return Collection<User>
*/
public function match(Criteria $criteria): Collection;
}
Loading