Skip to content

Commit e92eb23

Browse files
committed
Support criteria
1 parent c72dc8b commit e92eb23

7 files changed

Lines changed: 162 additions & 17 deletions

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
}
1313
],
1414
"require": {
15-
"php": ">=7.0"
15+
"php": ">=7.0",
16+
"doctrine/common": "^2.9"
1617
},
1718
"require-dev": {
1819
"phpunit/phpunit": "^6.4",

src/AndSpecification.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Tanigami\Specification;
44

5+
use Doctrine\Common\Collections\Criteria;
6+
57
class AndSpecification extends Specification
68
{
79
/**
@@ -32,6 +34,14 @@ public function isSatisfiedBy($object): bool
3234
return $this->one->isSatisfiedBy($object) && $this->other->isSatisfiedBy($object);
3335
}
3436

37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function criteria(): Criteria
41+
{
42+
return $this->one->criteria()->andWhere($this->other->criteria()->getWhereExpression());
43+
}
44+
3545
/**
3646
* @return Specification
3747
*/

src/AnyOfSpecification.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Tanigami\Specification;
44

5+
use Doctrine\Common\Collections\Criteria;
6+
57
class AnyOfSpecification extends Specification
68
{
79
/**
@@ -31,6 +33,24 @@ public function isSatisfiedBy($object): bool
3133
return true;
3234
}
3335

36+
/**
37+
* {@inheritdoc}
38+
*/
39+
public function criteria(): Criteria
40+
{
41+
/** @var Criteria $criteria */
42+
$criteria = null;
43+
foreach ($this->specifications as $specification) {
44+
if (is_null($criteria)) {
45+
$criteria = $specification->criteria();
46+
} else {
47+
$criteria = $criteria->andWhere($specification->criteria()->getWhereExpression());
48+
}
49+
}
50+
51+
return $criteria;
52+
}
53+
3454
/**
3555
* @return Specification[]
3656
*/

src/OneOfSpecification.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Tanigami\Specification;
44

5+
use Doctrine\Common\Collections\Criteria;
6+
57
class OneOfSpecification extends Specification
68
{
79
/**
@@ -17,6 +19,24 @@ public function __construct(Specification ...$specifications)
1719
$this->specifications = $specifications;
1820
}
1921

22+
/**
23+
* {@inheritdoc}
24+
*/
25+
public function criteria(): Criteria
26+
{
27+
/** @var Criteria $criteria */
28+
$criteria = null;
29+
foreach ($this->specifications as $specification) {
30+
if (is_null($criteria)) {
31+
$criteria = $specification->criteria();
32+
} else {
33+
$criteria = $criteria->orWhere($specification->criteria()->getWhereExpression());
34+
}
35+
}
36+
37+
return $criteria;
38+
}
39+
2040
/**
2141
* {@inheritdoc}
2242
*/

src/OrSpecification.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Tanigami\Specification;
44

5+
use Doctrine\Common\Collections\Criteria;
6+
57
class OrSpecification extends Specification
68
{
79
/**
@@ -32,6 +34,14 @@ public function isSatisfiedBy($object): bool
3234
return $this->one->isSatisfiedBy($object) || $this->other->isSatisfiedBy($object);
3335
}
3436

37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function criteria(): Criteria
41+
{
42+
return $this->one->criteria()->orWhere($this->other->criteria()->getWhereExpression());
43+
}
44+
3545
/**
3646
* @return Specification
3747
*/

src/Specification.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace Tanigami\Specification;
44

5+
use BadMethodCallException;
6+
use Doctrine\Common\Collections\Criteria;
7+
58
abstract class Specification
69
{
710
/**
@@ -10,6 +13,14 @@ abstract class Specification
1013
*/
1114
abstract public function isSatisfiedBy($object): bool;
1215

16+
/**
17+
* @return Criteria
18+
*/
19+
public function criteria()
20+
{
21+
throw new BadMethodCallException('Criteria is not supported.');
22+
}
23+
1324
/**
1425
* @param Specification $specification
1526
* @return AndSpecification
@@ -35,4 +46,4 @@ public function not(): NotSpecification
3546
{
3647
return new NotSpecification($this);
3748
}
38-
}
49+
}

tests/SpecificationTest.php

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,29 @@
22

33
namespace Tanigami\Specification;
44

5+
use Doctrine\Common\Collections\Criteria;
6+
use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
7+
use Doctrine\Common\Collections\Expr\Comparison;
8+
use Doctrine\Common\Collections\Expr\CompositeExpression;
9+
use Doctrine\Common\Collections\Expr\ExpressionVisitor;
10+
use Doctrine\Common\Collections\Expr\Value;
511
use PHPUnit\Framework\TestCase;
612
use stdClass;
713

814
class SpecificationTest extends TestCase
915
{
1016
public function testSpecification()
1117
{
12-
$trueSpec = new FakeSpecification(true);
13-
$falseSpec = new FakeSpecification(false);
18+
$trueSpec = new BoolSpecification(true);
19+
$falseSpec = new BoolSpecification(false);
1420
$this->assertTrue($trueSpec->isSatisfiedBy(new stdClass));
1521
$this->assertFalse($falseSpec->isSatisfiedBy(new stdClass));
1622
}
1723

1824
public function testNotSpecification()
1925
{
20-
$trueSpec = new FakeSpecification(true);
21-
$falseSpec = new FakeSpecification(false);
26+
$trueSpec = new BoolSpecification(true);
27+
$falseSpec = new BoolSpecification(false);
2228
$notTrueSpec = $trueSpec->not();
2329
$notFalseSpec = $falseSpec->not();
2430
$this->assertFalse($notTrueSpec->isSatisfiedBy(new stdClass));
@@ -27,8 +33,8 @@ public function testNotSpecification()
2733

2834
public function testAndSpecification()
2935
{
30-
$trueSpec = new FakeSpecification(true);
31-
$falseSpec = new FakeSpecification(false);
36+
$trueSpec = new BoolSpecification(true);
37+
$falseSpec = new BoolSpecification(false);
3238
$trueAndTrueSpec = $trueSpec->and($trueSpec);
3339
$trueAndFalseSpec = $trueSpec->and($falseSpec);
3440
$this->assertTrue($trueAndTrueSpec->isSatisfiedBy(new stdClass));
@@ -37,8 +43,8 @@ public function testAndSpecification()
3743

3844
public function testOrSpecification()
3945
{
40-
$trueSpec = new FakeSpecification(true);
41-
$falseSpec = new FakeSpecification(false);
46+
$trueSpec = new BoolSpecification(true);
47+
$falseSpec = new BoolSpecification(false);
4248
$trueOrTrueSpec = $trueSpec->or($trueSpec);
4349
$trueOrFalseSpec = $trueSpec->or($falseSpec);
4450
$this->assertTrue($trueOrTrueSpec->isSatisfiedBy(new stdClass));
@@ -47,30 +53,56 @@ public function testOrSpecification()
4753

4854
public function testAnyOfSpecification()
4955
{
50-
$trueSpec = new FakeSpecification(true);
51-
$falseSpec = new FakeSpecification(false);
56+
$trueSpec = new BoolSpecification(true);
57+
$falseSpec = new BoolSpecification(false);
5258
$this->assertTrue((new AnyOfSpecification($trueSpec, $trueSpec, $trueSpec))->isSatisfiedBy(new stdClass));
5359
$this->assertFalse((new AnyOfSpecification($trueSpec, $trueSpec, $falseSpec))->isSatisfiedBy(new stdClass));
5460
}
5561

5662
public function testOneOfSpecification()
5763
{
58-
$trueSpec = new FakeSpecification(true);
59-
$falseSpec = new FakeSpecification(false);
64+
$trueSpec = new BoolSpecification(true);
65+
$falseSpec = new BoolSpecification(false);
6066
$this->assertFalse((new OneOfSpecification($falseSpec, $falseSpec, $falseSpec))->isSatisfiedBy(new stdClass));
6167
$this->assertTrue((new OneOfSpecification($falseSpec, $falseSpec, $trueSpec))->isSatisfiedBy(new stdClass));
6268
}
6369

6470
public function testNoneOfSpecification()
6571
{
66-
$trueSpec = new FakeSpecification(true);
67-
$falseSpec = new FakeSpecification(false);
72+
$trueSpec = new BoolSpecification(true);
73+
$falseSpec = new BoolSpecification(false);
6874
$this->assertTrue((new NoneOfSpecification($falseSpec, $falseSpec, $falseSpec))->isSatisfiedBy(new stdClass));
6975
$this->assertFalse((new NoneOfSpecification($falseSpec, $falseSpec, $trueSpec))->isSatisfiedBy(new stdClass));
7076
}
77+
78+
public function testCriteriaComposition()
79+
{
80+
$trueSpec = new BoolSpecification(true);
81+
$falseSpec = new BoolSpecification(false);
82+
$compositeSpec =
83+
new AnyOfSpecification(
84+
$trueSpec->and($falseSpec)->or($trueSpec)->and($falseSpec),
85+
new OneOfSpecification($trueSpec, $falseSpec, $trueSpec),
86+
$trueSpec
87+
);
88+
$visitor = new Visitor();
89+
$compositeSpec->criteria()->getWhereExpression()->visit($visitor);
90+
$this->assertSame(
91+
'((((((bool = 1) AND (bool = 0)) OR (bool = 1)) AND (bool = 0)) AND (((bool = 1) OR (bool = 0)) OR (bool = 1))) AND (bool = 1))',
92+
$visitor->trace()
93+
);
94+
}
95+
96+
/**
97+
* @expectedException \BadMethodCallException
98+
*/
99+
public function testCriteriaIsNotSupported()
100+
{
101+
(new BoolSpecification(true))->not()->criteria();
102+
}
71103
}
72104

73-
class FakeSpecification extends Specification
105+
class BoolSpecification extends Specification
74106
{
75107
private $bool;
76108

@@ -83,4 +115,45 @@ public function isSatisfiedBy($object): bool
83115
{
84116
return $this->bool;
85117
}
118+
119+
public function criteria(): Criteria
120+
{
121+
return new Criteria(Criteria::expr()->eq('bool', $this->bool));
122+
}
123+
}
124+
125+
class Visitor extends ExpressionVisitor
126+
{
127+
private $trace;
128+
129+
public function walkComparison(Comparison $comparison)
130+
{
131+
$this->trace .= '(';
132+
$this->trace .= $comparison->getField();
133+
$this->trace .= ' ' . $comparison->getOperator() . ' ';
134+
$this->trace .= $this->walkValue($comparison->getValue());
135+
$this->trace .= ')';
136+
}
137+
138+
public function walkCompositeExpression(CompositeExpression $expr)
139+
{
140+
$this->trace .= '(';
141+
foreach ($expr->getExpressionList() as $i => $child) {
142+
if ($i !== 0) {
143+
$this->trace .= (' ' . $expr->getType() . ' ');
144+
}
145+
$expressionList[] = $this->dispatch($child);
146+
}
147+
$this->trace .= ')';
148+
}
149+
150+
public function walkValue(Value $value)
151+
{
152+
return $value->getValue() ? '1' : '0';
153+
}
154+
155+
public function trace()
156+
{
157+
return $this->trace;
158+
}
86159
}

0 commit comments

Comments
 (0)