Skip to content

Commit 078ca37

Browse files
Feature: inherit contracts (#31)
* Made AbstractFetcher accept more then one annotation type * Add inherit annotation This will serve as a contract inherit flag. It will take all existing annotations (ensure, verify, invariant) and check current as well as parent classes for contracts * Update Inherit implementation - Inherit will now always inherit contracts (despite missing @inheritdoc) - provide Inherit tests - update Demo implementation - update Readme with Inherit documentation
1 parent 51150fd commit 078ca37

18 files changed

+376
-27
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/vendor/
22
composer.lock
33
/tests/cache/
4+
/demo/cache/

README.md

+52-11
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,30 @@ class Account
135135
```
136136
Invariants contain assert expressions, and so when they fail, they throw a ContractViolation exception.
137137

138-
NOTE! The code in the invariant may not call any public non-static members of the class, either directly or
138+
__NOTE__: The code in the invariant may not call any public non-static members of the class, either directly or
139139
indirectly. Doing so will result in a stack overflow, as the invariant will wind up being called in an
140140
infinitely recursive manner.
141141

142142
Contract propagation
143143
----------
144144

145-
All contracts are propagated from parent classes and interfaces.
146-
147-
For preconditions (Verify contracts) subclasses do not inherit contracts of parents' methods if they don't have the @inheritdoc annotation. Example:
148-
145+
There a some differences in inheritance of the contracts:
146+
147+
1. Ensure
148+
- if provided `Ensure` will automatically inherit all contracts from parent class or interface
149+
2. Verify
150+
- if provided `Verify` will _not_ inherit contracts from parent class or interface
151+
- to inherit contracts you will ne to provide `@inheritdoc` or the `Inherit` contract
152+
3. Invariant
153+
- if provided `Invariant` will inherit all contracts from parent class or interface
154+
4. Inherit
155+
- if provided `Inherit` will inherit all contracts from the given leven (class, method) without the
156+
need to provide a contract on your current class or method
157+
158+
__Notes__:
159+
- The parsing of a contract only happens __IF__ you provide any given annotation from this package.
160+
Without it your contracts won't work!
161+
- The annotation __must not__ have curly braces (`{}`) otherwise the annotation reader can't find them.
149162

150163
```php
151164

@@ -175,7 +188,7 @@ class FooParent
175188

176189
```
177190

178-
Foo::bar accepts '2' literal as a parameter and does not accept '1'.
191+
`Foo::bar` accepts `2` literal as a parameter and does not accept `1`.
179192

180193
With @inheritdoc:
181194

@@ -208,12 +221,10 @@ class FooParent
208221

209222
```
210223

211-
Foo::bar does not accept '1' and '2' literals as a parameter.
212-
213-
224+
`Foo::bar` does not accept `1` and `2` literals as a parameter.
214225

215226

216-
For postconditions (Ensure and Invariants contracts) subclasses inherit contracts and they don't need @inheritdoc. Example:
227+
For postconditions (Ensure and Invariants contracts) subclasses inherit contracts and they don't need `@inheritdoc`. Example:
217228

218229
```php
219230

@@ -246,7 +257,37 @@ class FooParent
246257

247258
```
248259

249-
Foo::setBar does not accept '1' and '2' literals as a parameter.
260+
`Foo::setBar` does not accept `1` and `2` literals as a parameter.
261+
262+
If you don't want to provide a contract on your curent method/class you can use the `Inherit` annotation:
263+
264+
```php
265+
class Foo extends FooParent
266+
{
267+
/**
268+
* @param int $amount
269+
* @Contract\Inherit
270+
*/
271+
public function bar($amount)
272+
{
273+
...
274+
}
275+
}
276+
277+
class FooParent
278+
{
279+
/**
280+
* @param int $amount
281+
* @Contract\Verify("$amount != 2")
282+
*/
283+
public function bar($amount)
284+
{
285+
...
286+
}
287+
}
288+
```
289+
290+
`Foo:bar()` does accept eveything, except: `2`
250291

251292
Integration with assertion library
252293
----------

demo/Demo/Account.php

+11-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
/**
1616
* Simple trade account class
17-
* @Contract\Invariant("$this->balance > 0")
17+
* @Contract\Invariant("$this->balance >= 0")
1818
*/
1919
class Account implements AccountContractInterface
2020
{
@@ -39,10 +39,20 @@ public function deposit($amount)
3939
$this->balance += $amount;
4040
}
4141

42+
/**
43+
* @Contract\Inherit()
44+
* @param float $amount
45+
*/
46+
public function withdraw($amount)
47+
{
48+
$this->balance -= $amount;
49+
}
50+
4251
/**
4352
* Returns current balance
4453
*
4554
* @Contract\Ensure("$__result == $this->balance")
55+
*
4656
* @return float
4757
*/
4858
public function getBalance()

demo/Demo/AccountContractInterface.php

+11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ interface AccountContractInterface
2727
*/
2828
public function deposit($amount);
2929

30+
/**
31+
* Withdraw amount of money from account.
32+
*
33+
* We don't allow withdrawal of more than 50
34+
* @Contract\Verify("$amount <= $this->balance")
35+
* @Contract\Verify("$amount <= 50")
36+
* @Contract\Ensure("$this->balance == $__old->balance-$amount")
37+
* @param float $amount
38+
*/
39+
public function withdraw($amount);
40+
3041
/**
3142
* Returns current balance
3243
*

demo/demo.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,11 @@
1313
include_once __DIR__.'/aspect_bootstrap.php';
1414

1515
$account = new Demo\Account();
16+
17+
echo 'Deposit: 100' . PHP_EOL;
1618
$account->deposit(100);
17-
echo $account->getBalance();
19+
echo 'Current balance: ' . $account->getBalance();
20+
echo PHP_EOL;
21+
echo 'Withdraw: 100' . PHP_EOL;
22+
$account->withdraw(50);
23+
echo 'Current balance: ' . $account->getBalance();

src/Annotation/Inherit.php

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
/**
3+
* PHP Deal framework
4+
*
5+
* @copyright Copyright 2014, Lisachenko Alexander <[email protected]>
6+
*
7+
* This source file is subject to the license that is bundled
8+
* with this source code in the file LICENSE.
9+
*/
10+
namespace PhpDeal\Annotation;
11+
12+
use Doctrine\Common\Annotations\Annotation as BaseAnnotation;
13+
14+
/**
15+
* This annotation defines a contract inheritance check, applied to the method or class
16+
*
17+
* @Annotation
18+
* @Target({"METHOD", "CLASS"})
19+
*/
20+
class Inherit extends BaseAnnotation
21+
{
22+
public function __toString()
23+
{
24+
return $this->value;
25+
}
26+
}

src/Aspect/AbstractContractAspect.php

+5
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ protected function fetchMethodArguments(MethodInvocation $invocation)
6767
* @param array $args List of arguments for the method
6868
*
6969
* @throws DomainException
70+
* @throws ContractViolation
7071
*/
7172
protected function ensureContracts(MethodInvocation $invocation, array $contracts, $instance, $scope, array $args)
7273
{
@@ -87,6 +88,10 @@ protected function ensureContracts(MethodInvocation $invocation, array $contract
8788
try {
8889
$invocationResult = $boundInvoker->__invoke($args, $contractExpression);
8990

91+
// if ($invocationResult === false) {
92+
// throw new ContractViolation($invocation, $contractExpression);
93+
// }
94+
9095
// we accept as a result only true or null
9196
// null may be a result of assertions from beberlei/assert which passed
9297
if ($invocationResult !== null && $invocationResult !== true) {

src/Aspect/InheritCheckerAspect.php

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
namespace PhpDeal\Aspect;
4+
5+
use Doctrine\Common\Annotations\Reader;
6+
use Go\Aop\Aspect;
7+
use Go\Aop\Intercept\MethodInvocation;
8+
use Go\Lang\Annotation\Around;
9+
use PhpDeal\Annotation\Ensure;
10+
use PhpDeal\Annotation\Invariant;
11+
use PhpDeal\Annotation\Verify;
12+
use PhpDeal\Contract\Fetcher\Parent\InvariantFetcher;
13+
use PhpDeal\Contract\Fetcher\Parent\MethodConditionFetcher;
14+
use PhpDeal\Exception\ContractViolation;
15+
use ReflectionClass;
16+
17+
class InheritCheckerAspect extends AbstractContractAspect implements Aspect
18+
{
19+
/**
20+
* @var MethodConditionFetcher
21+
*/
22+
private $methodConditionFetcher;
23+
24+
/** @var InvariantFetcher */
25+
private $invariantFetcher;
26+
27+
public function __construct(Reader $reader)
28+
{
29+
parent::__construct($reader);
30+
$this->methodConditionFetcher = new MethodConditionFetcher([Ensure::class, Verify::class, Invariant::class], $reader);
31+
$this->invariantFetcher = new InvariantFetcher([Invariant::class], $reader);
32+
}
33+
34+
/**
35+
* Verifies inherit contracts for the method
36+
*
37+
* @Around("@execution(PhpDeal\Annotation\Inherit)")
38+
* @param MethodInvocation $invocation
39+
*
40+
* @throws ContractViolation
41+
* @return mixed
42+
*/
43+
public function inheritMethodContracts(MethodInvocation $invocation)
44+
{
45+
$object = $invocation->getThis();
46+
$args = $this->fetchMethodArguments($invocation);
47+
$class = $invocation->getMethod()->getDeclaringClass();
48+
if ($class->isCloneable()) {
49+
$args['__old'] = clone $object;
50+
}
51+
52+
$result = $invocation->proceed();
53+
$args['__result'] = $result;
54+
$allContracts = $this->fetchMethodContracts($invocation);
55+
56+
$this->ensureContracts($invocation, $allContracts, $object, $class->name, $args);
57+
58+
return $result;
59+
}
60+
61+
/**
62+
* @Around("@within(PhpDeal\Annotation\Inherit) && execution(public **->*(*))")
63+
* @param MethodInvocation $invocation
64+
* @return mixed
65+
*/
66+
public function inheritClassContracts(MethodInvocation $invocation)
67+
{
68+
$object = $invocation->getThis();
69+
$args = $this->fetchMethodArguments($invocation);
70+
$class = $invocation->getMethod()->getDeclaringClass();
71+
if ($class->isCloneable()) {
72+
$args['__old'] = clone $object;
73+
}
74+
75+
$result = $invocation->proceed();
76+
$args['__result'] = $result;
77+
78+
$allContracts = $this->fetchClassContracts($class);
79+
$this->ensureContracts($invocation, $allContracts, $object, $class->name, $args);
80+
81+
return $result;
82+
}
83+
84+
/**
85+
* @param MethodInvocation $invocation
86+
* @return array
87+
*/
88+
private function fetchMethodContracts(MethodInvocation $invocation)
89+
{
90+
$allContracts = $this->fetchParentsMethodContracts($invocation);
91+
92+
foreach ($invocation->getMethod()->getAnnotations() as $annotation) {
93+
$annotationClass = \get_class($annotation);
94+
95+
if (\in_array($annotationClass, [Ensure::class, Verify::class, Invariant::class], true)) {
96+
$allContracts[] = $annotation;
97+
}
98+
}
99+
100+
return array_unique($allContracts);
101+
}
102+
103+
/**
104+
* @param MethodInvocation $invocation
105+
* @return array
106+
*/
107+
private function fetchParentsMethodContracts(MethodInvocation $invocation)
108+
{
109+
return $this->methodConditionFetcher->getConditions(
110+
$invocation->getMethod()->getDeclaringClass(),
111+
$invocation->getMethod()->name
112+
);
113+
}
114+
115+
/**
116+
* @param ReflectionClass $class
117+
* @return array
118+
*/
119+
private function fetchClassContracts(ReflectionClass $class)
120+
{
121+
$allContracts = $this->invariantFetcher->getConditions($class);
122+
foreach ($this->reader->getClassAnnotations($class) as $annotation) {
123+
if ($annotation instanceof Invariant) {
124+
$allContracts[] = $annotation;
125+
}
126+
}
127+
128+
return array_unique($allContracts);
129+
}
130+
}

src/Aspect/InvariantCheckerAspect.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class InvariantCheckerAspect extends AbstractContractAspect implements Aspect
2929
public function __construct(Reader $reader)
3030
{
3131
parent::__construct($reader);
32-
$this->invariantFetcher = new InvariantFetcher(Invariant::class, $reader);
32+
$this->invariantFetcher = new InvariantFetcher([Invariant::class], $reader);
3333
}
3434

3535
/**

src/Aspect/PostconditionCheckerAspect.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class PostconditionCheckerAspect extends AbstractContractAspect implements Aspec
2828
public function __construct(Reader $reader)
2929
{
3030
parent::__construct($reader);
31-
$this->methodConditionFetcher = new MethodConditionFetcher(Ensure::class, $reader);
31+
$this->methodConditionFetcher = new MethodConditionFetcher([Ensure::class], $reader);
3232
}
3333

3434
/**

src/Aspect/PreconditionCheckerAspect.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class PreconditionCheckerAspect extends AbstractContractAspect implements Aspect
2828
public function __construct(Reader $reader)
2929
{
3030
parent::__construct($reader);
31-
$this->methodConditionFetcher = new MethodConditionWithInheritDocFetcher(Verify::class, $reader);
31+
$this->methodConditionFetcher = new MethodConditionWithInheritDocFetcher([Verify::class], $reader);
3232
}
3333

3434
/**

0 commit comments

Comments
 (0)