Skip to content

Commit dfb02c0

Browse files
authored
Initial version
1 parent 7a6a4f5 commit dfb02c0

27 files changed

Lines changed: 1797 additions & 11 deletions

.docs/README.md

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# Contributte / Nextras Criteria
2+
3+
Criteria pattern for [Nextras ORM](https://nextras.org/orm), inspired by [Doctrine Criteria API](https://www.doctrine-project.org/projects/doctrine-collections/en/latest/expressions.html).
4+
5+
## Content
6+
7+
- [Installation](#installation)
8+
- [Usage](#usage)
9+
- [Expression Builder](#expression-builder)
10+
- [Ordering](#ordering)
11+
- [Pagination](#pagination)
12+
- [Examples](#examples)
13+
14+
## Installation
15+
16+
Install package using composer.
17+
18+
```bash
19+
composer require contributte/nextras-criteria
20+
```
21+
22+
## Usage
23+
24+
### Basic usage
25+
26+
```php
27+
use Contributte\Criteria\Criteria;
28+
use Contributte\Criteria\Ordering;
29+
use Contributte\Criteria\Nextras\CriteriaApplicator;
30+
31+
// Create criteria
32+
$criteria = Criteria::create()
33+
->where(Criteria::expr()->eq('status', 'active'))
34+
->andWhere(Criteria::expr()->gt('age', 18))
35+
->orderBy(Ordering::desc('createdAt'))
36+
->setMaxResults(10);
37+
38+
// Apply to Nextras collection
39+
$applicator = new CriteriaApplicator();
40+
$users = $applicator->apply($orm->users->findAll(), $criteria);
41+
```
42+
43+
### Advanced usage
44+
45+
```php
46+
use Contributte\Criteria\Criteria;
47+
use Contributte\Criteria\Ordering;
48+
use Contributte\Criteria\Nextras\CriteriaApplicator;
49+
50+
class UserRepository
51+
{
52+
private CriteriaApplicator $applicator;
53+
54+
public function __construct(
55+
private UserOrmRepository $ormRepository,
56+
) {
57+
$this->applicator = new CriteriaApplicator();
58+
}
59+
60+
public function findActiveAdults(int $page, int $perPage): array
61+
{
62+
$criteria = Criteria::create()
63+
->where(Criteria::expr()->andX(
64+
Criteria::expr()->eq('status', 'active'),
65+
Criteria::expr()->gte('age', 18),
66+
Criteria::expr()->isNotNull('verifiedAt')
67+
))
68+
->orderBy([
69+
Ordering::desc('createdAt'),
70+
Ordering::asc('lastName'),
71+
])
72+
->setFirstResult(($page - 1) * $perPage)
73+
->setMaxResults($perPage);
74+
75+
return $this->applicator
76+
->apply($this->ormRepository->findAll(), $criteria)
77+
->fetchAll();
78+
}
79+
}
80+
```
81+
82+
## Expression Builder
83+
84+
The `Criteria::expr()` method returns an `ExpressionBuilder` for creating filter expressions.
85+
86+
### Comparison operators
87+
88+
```php
89+
$expr = Criteria::expr();
90+
91+
// Equality
92+
$expr->eq('name', 'John'); // name = 'John'
93+
$expr->neq('status', 'deleted'); // status != 'deleted'
94+
95+
// Comparison
96+
$expr->lt('age', 18); // age < 18
97+
$expr->lte('price', 100); // price <= 100
98+
$expr->gt('rating', 4); // rating > 4
99+
$expr->gte('quantity', 10); // quantity >= 10
100+
101+
// IN / NOT IN
102+
$expr->in('status', ['active', 'pending']);
103+
$expr->notIn('role', ['banned', 'suspended']);
104+
105+
// LIKE patterns
106+
$expr->contains('description', 'keyword'); // LIKE '%keyword%'
107+
$expr->startsWith('email', 'admin@'); // LIKE 'admin@%'
108+
$expr->endsWith('email', '@example.com'); // LIKE '%@example.com'
109+
110+
// NULL checks
111+
$expr->isNull('deletedAt');
112+
$expr->isNotNull('verifiedAt');
113+
```
114+
115+
### Composite expressions (AND/OR)
116+
117+
```php
118+
$expr = Criteria::expr();
119+
120+
// AND condition
121+
$criteria = Criteria::create()->where(
122+
$expr->andX(
123+
$expr->eq('status', 'active'),
124+
$expr->gt('age', 18),
125+
$expr->isNotNull('email')
126+
)
127+
);
128+
129+
// OR condition
130+
$criteria = Criteria::create()->where(
131+
$expr->orX(
132+
$expr->eq('role', 'admin'),
133+
$expr->eq('role', 'moderator')
134+
)
135+
);
136+
137+
// Nested conditions: (status = 'active' AND age > 18) OR role = 'admin'
138+
$criteria = Criteria::create()->where(
139+
$expr->orX(
140+
$expr->andX(
141+
$expr->eq('status', 'active'),
142+
$expr->gt('age', 18)
143+
),
144+
$expr->eq('role', 'admin')
145+
)
146+
);
147+
```
148+
149+
### Fluent where methods
150+
151+
```php
152+
$criteria = Criteria::create()
153+
->where(Criteria::expr()->eq('status', 'active'))
154+
->andWhere(Criteria::expr()->gt('age', 18))
155+
->orWhere(Criteria::expr()->eq('role', 'admin'));
156+
```
157+
158+
### Relationship traversal
159+
160+
Nextras ORM supports filtering by related entity properties using `->` notation:
161+
162+
```php
163+
// Filter books by author's name
164+
$criteria = Criteria::create()
165+
->where(Criteria::expr()->eq('author->name', 'Jon Snow'));
166+
167+
// Order by related entity
168+
$criteria = Criteria::create()
169+
->orderBy(Ordering::asc('author->lastName'));
170+
```
171+
172+
## Ordering
173+
174+
```php
175+
use Contributte\Criteria\Ordering;
176+
177+
// Single ordering
178+
$criteria = Criteria::create()
179+
->orderBy(Ordering::desc('createdAt'));
180+
181+
// Multiple orderings
182+
$criteria = Criteria::create()
183+
->orderBy([
184+
Ordering::asc('lastName'),
185+
Ordering::asc('firstName'),
186+
]);
187+
188+
// Add ordering
189+
$criteria = Criteria::create()
190+
->orderBy(Ordering::desc('priority'))
191+
->addOrderBy(Ordering::asc('name'));
192+
```
193+
194+
## Pagination
195+
196+
```php
197+
$criteria = Criteria::create()
198+
->setFirstResult(20) // offset
199+
->setMaxResults(10); // limit
200+
```
201+
202+
## Examples
203+
204+
### Reusable criteria
205+
206+
Create reusable criteria specifications:
207+
208+
```php
209+
class ActiveUserCriteria
210+
{
211+
public static function create(): Criteria
212+
{
213+
return Criteria::create()
214+
->where(Criteria::expr()->andX(
215+
Criteria::expr()->eq('status', 'active'),
216+
Criteria::expr()->isNull('deletedAt')
217+
));
218+
}
219+
}
220+
221+
// Combine criteria
222+
$criteria = ActiveUserCriteria::create()
223+
->andWhere(Criteria::expr()->gte('age', 18))
224+
->orderBy(Ordering::desc('createdAt'));
225+
```
226+
227+
> [!TIP]
228+
> Take a look at more examples in [contributte/playground](https://github.com/contributte/playground).

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ indent_style = tab
1111
indent_size = tab
1212
tab_width = 4
1313

14-
[{*.json, *.yaml, *.yml, *.md}]
14+
[*.{json,yaml,yml,md}]
1515
indent_style = space
1616
indent_size = 2

.gitattributes

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.docs export-ignore
2+
.editorconfig export-ignore
3+
.gitattributes export-ignore
4+
.gitignore export-ignore
5+
Makefile export-ignore
6+
phpstan.neon export-ignore
7+
ruleset.xml export-ignore
8+
tests export-ignore

.github/.kodiak.toml

Lines changed: 0 additions & 10 deletions
This file was deleted.

.github/workflows/codesniffer.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: "Codesniffer"
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
push:
7+
branches: ["*"]
8+
schedule:
9+
- cron: "0 8 * * 1"
10+
11+
jobs:
12+
codesniffer:
13+
name: "Codesniffer"
14+
uses: contributte/.github/.github/workflows/codesniffer.yml@master
15+
with:
16+
php: "8.2"

.github/workflows/coverage.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: "Coverage"
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
push:
7+
branches: ["*"]
8+
schedule:
9+
- cron: "0 9 * * 1"
10+
11+
jobs:
12+
coverage:
13+
name: "Nette Tester"
14+
uses: contributte/.github/.github/workflows/nette-tester-coverage-v2.yml@master
15+
with:
16+
php: "8.2"

.github/workflows/phpstan.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: "Phpstan"
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
push:
7+
branches: ["*"]
8+
schedule:
9+
- cron: "0 10 * * 1"
10+
11+
jobs:
12+
phpstan:
13+
name: "Phpstan"
14+
uses: contributte/.github/.github/workflows/phpstan.yml@master
15+
with:
16+
php: "8.2"

.github/workflows/tests.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: "Nette Tester"
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
push:
7+
branches: ["*"]
8+
schedule:
9+
- cron: "0 10 * * 1"
10+
11+
jobs:
12+
test85:
13+
name: "Nette Tester"
14+
uses: contributte/.github/.github/workflows/nette-tester.yml@master
15+
with:
16+
php: "8.5"
17+
18+
test84:
19+
name: "Nette Tester"
20+
uses: contributte/.github/.github/workflows/nette-tester.yml@master
21+
with:
22+
php: "8.4"
23+
24+
test83:
25+
name: "Nette Tester"
26+
uses: contributte/.github/.github/workflows/nette-tester.yml@master
27+
with:
28+
php: "8.3"
29+
30+
test82:
31+
name: "Nette Tester"
32+
uses: contributte/.github/.github/workflows/nette-tester.yml@master
33+
with:
34+
php: "8.2"
35+
36+
testlower:
37+
name: "Nette Tester"
38+
uses: contributte/.github/.github/workflows/nette-tester.yml@master
39+
with:
40+
php: "8.2"
41+
composer: "composer update --no-interaction --no-progress --prefer-dist --prefer-stable --prefer-lowest"

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/.idea
2+
/vendor
3+
/composer.lock
4+
5+
/tests/tmp
6+
/tests/*.log
7+
/tests/*.html
8+
/tests/*.actual
9+
/tests/*.expected
10+
11+
/coverage.html
12+
/coverage.xml

Makefile

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.PHONY: install
2+
install:
3+
composer update
4+
5+
.PHONY: qa
6+
qa: phpstan cs
7+
8+
.PHONY: cs
9+
cs:
10+
ifdef GITHUB_ACTION
11+
vendor/bin/phpcs --standard=ruleset.xml --extensions="php,phpt" --encoding=utf-8 --colors -nsp -q --report=checkstyle src tests | cs2pr
12+
else
13+
vendor/bin/phpcs --standard=ruleset.xml --extensions="php,phpt" --encoding=utf-8 --colors -nsp src tests
14+
endif
15+
16+
.PHONY: csf
17+
csf:
18+
vendor/bin/phpcbf --standard=ruleset.xml --extensions="php,phpt" --encoding=utf-8 --colors -nsp src tests
19+
20+
.PHONY: phpstan
21+
phpstan:
22+
vendor/bin/phpstan analyse -c phpstan.neon
23+
24+
.PHONY: tests
25+
tests:
26+
vendor/bin/tester -s -p php -C tests/Cases
27+
28+
.PHONY: coverage
29+
coverage:
30+
ifdef GITHUB_ACTION
31+
vendor/bin/tester -s -p phpdbg -C --coverage coverage.xml --coverage-src src tests/Cases
32+
else
33+
vendor/bin/tester -s -p phpdbg -C --coverage coverage.html --coverage-src src tests/Cases
34+
endif

0 commit comments

Comments
 (0)