Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/vendor/
composer.lock
/tests/cache/
/demo/cache/
63 changes: 52 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,30 @@ class Account
```
Invariants contain assert expressions, and so when they fail, they throw a ContractViolation exception.

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

Contract propagation
----------

All contracts are propagated from parent classes and interfaces.

For preconditions (Verify contracts) subclasses do not inherit contracts of parents' methods if they don't have the @inheritdoc annotation. Example:

There a some differences in inheritance of the contracts:

1. Ensure
- if provided `Ensure` will automatically inherit all contracts from parent class or interface
2. Verify
- if provided `Verify` will _not_ inherit contracts from parent class or interface
- to inherit contracts you will ne to provide `@inheritdoc` or the `Inherit` contract
3. Invariant
- if provided `Invariant` will inherit all contracts from parent class or interface
4. Inherit
- if provided `Inherit` will inherit all contracts from the given leven (class, method) without the
need to provide a contract on your current class or method

__Notes__:
- The parsing of a contract only happens __IF__ you provide any given annotation from this package.
Without it your contracts won't work!
- The annotation __must not__ have curly braces (`{}`) otherwise the annotation reader can't find them.

```php

Expand Down Expand Up @@ -175,7 +188,7 @@ class FooParent

```

Foo::bar accepts '2' literal as a parameter and does not accept '1'.
`Foo::bar` accepts `2` literal as a parameter and does not accept `1`.

With @inheritdoc:

Expand Down Expand Up @@ -208,12 +221,10 @@ class FooParent

```

Foo::bar does not accept '1' and '2' literals as a parameter.


`Foo::bar` does not accept `1` and `2` literals as a parameter.


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

```php

Expand Down Expand Up @@ -246,7 +257,37 @@ class FooParent

```

Foo::setBar does not accept '1' and '2' literals as a parameter.
`Foo::setBar` does not accept `1` and `2` literals as a parameter.

If you don't want to provide a contract on your curent method/class you can use the `Inherit` annotation:

```php
class Foo extends FooParent
{
/**
* @param int $amount
* @Contract\Inherit
*/
public function bar($amount)
{
...
}
}

class FooParent
{
/**
* @param int $amount
* @Contract\Verify("$amount != 2")
*/
public function bar($amount)
{
...
}
}
```

`Foo:bar()` does accept eveything, except: `2`

Integration with assertion library
----------
Expand Down
12 changes: 11 additions & 1 deletion demo/Demo/Account.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

/**
* Simple trade account class
* @Contract\Invariant("$this->balance > 0")
* @Contract\Invariant("$this->balance >= 0")
*/
class Account implements AccountContractInterface
{
Expand All @@ -39,10 +39,20 @@ public function deposit($amount)
$this->balance += $amount;
}

/**
* @Contract\Inherit()
* @param float $amount
*/
public function withdraw($amount)
{
$this->balance -= $amount;
}

/**
* Returns current balance
*
* @Contract\Ensure("$__result == $this->balance")
*
* @return float
*/
public function getBalance()
Expand Down
11 changes: 11 additions & 0 deletions demo/Demo/AccountContractInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ interface AccountContractInterface
*/
public function deposit($amount);

/**
* Withdraw amount of money from account.
*
* We don't allow withdrawal of more than 50
* @Contract\Verify("$amount <= $this->balance")
* @Contract\Verify("$amount <= 50")
* @Contract\Ensure("$this->balance == $__old->balance-$amount")
* @param float $amount
*/
public function withdraw($amount);

/**
* Returns current balance
*
Expand Down
8 changes: 7 additions & 1 deletion demo/demo.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,11 @@
include_once __DIR__.'/aspect_bootstrap.php';

$account = new Demo\Account();

echo 'Deposit: 100' . PHP_EOL;
$account->deposit(100);
echo $account->getBalance();
echo 'Current balance: ' . $account->getBalance();
echo PHP_EOL;
echo 'Withdraw: 100' . PHP_EOL;
$account->withdraw(50);
echo 'Current balance: ' . $account->getBalance();
26 changes: 26 additions & 0 deletions src/Annotation/Inherit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
/**
* PHP Deal framework
*
* @copyright Copyright 2014, Lisachenko Alexander <lisachenko.it@gmail.com>
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpDeal\Annotation;

use Doctrine\Common\Annotations\Annotation as BaseAnnotation;

/**
* This annotation defines a contract inheritance check, applied to the method or class
*
* @Annotation
* @Target({"METHOD", "CLASS"})
*/
class Inherit extends BaseAnnotation
{
public function __toString()
{
return $this->value;
}
}
5 changes: 5 additions & 0 deletions src/Aspect/AbstractContractAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ protected function fetchMethodArguments(MethodInvocation $invocation)
* @param array $args List of arguments for the method
*
* @throws DomainException
* @throws ContractViolation
*/
protected function ensureContracts(MethodInvocation $invocation, array $contracts, $instance, $scope, array $args)
{
Expand All @@ -87,6 +88,10 @@ protected function ensureContracts(MethodInvocation $invocation, array $contract
try {
$invocationResult = $boundInvoker->__invoke($args, $contractExpression);

// if ($invocationResult === false) {
// throw new ContractViolation($invocation, $contractExpression);
// }

// we accept as a result only true or null
// null may be a result of assertions from beberlei/assert which passed
if ($invocationResult !== null && $invocationResult !== true) {
Expand Down
130 changes: 130 additions & 0 deletions src/Aspect/InheritCheckerAspect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

namespace PhpDeal\Aspect;

use Doctrine\Common\Annotations\Reader;
use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\Around;
use PhpDeal\Annotation\Ensure;
use PhpDeal\Annotation\Invariant;
use PhpDeal\Annotation\Verify;
use PhpDeal\Contract\Fetcher\Parent\InvariantFetcher;
use PhpDeal\Contract\Fetcher\Parent\MethodConditionFetcher;
use PhpDeal\Exception\ContractViolation;
use ReflectionClass;

class InheritCheckerAspect extends AbstractContractAspect implements Aspect
{
/**
* @var MethodConditionFetcher
*/
private $methodConditionFetcher;

/** @var InvariantFetcher */
private $invariantFetcher;

public function __construct(Reader $reader)
{
parent::__construct($reader);
$this->methodConditionFetcher = new MethodConditionFetcher([Ensure::class, Verify::class, Invariant::class], $reader);
$this->invariantFetcher = new InvariantFetcher([Invariant::class], $reader);
}

/**
* Verifies inherit contracts for the method
*
* @Around("@execution(PhpDeal\Annotation\Inherit)")
* @param MethodInvocation $invocation
*
* @throws ContractViolation
* @return mixed
*/
public function inheritMethodContracts(MethodInvocation $invocation)
{
$object = $invocation->getThis();
$args = $this->fetchMethodArguments($invocation);
$class = $invocation->getMethod()->getDeclaringClass();
if ($class->isCloneable()) {
$args['__old'] = clone $object;
}

$result = $invocation->proceed();
$args['__result'] = $result;
$allContracts = $this->fetchMethodContracts($invocation);

$this->ensureContracts($invocation, $allContracts, $object, $class->name, $args);

return $result;
}

/**
* @Around("@within(PhpDeal\Annotation\Inherit) && execution(public **->*(*))")
* @param MethodInvocation $invocation
* @return mixed
*/
public function inheritClassContracts(MethodInvocation $invocation)
{
$object = $invocation->getThis();
$args = $this->fetchMethodArguments($invocation);
$class = $invocation->getMethod()->getDeclaringClass();
if ($class->isCloneable()) {
$args['__old'] = clone $object;
}

$result = $invocation->proceed();
$args['__result'] = $result;

$allContracts = $this->fetchClassContracts($class);
$this->ensureContracts($invocation, $allContracts, $object, $class->name, $args);

return $result;
}

/**
* @param MethodInvocation $invocation
* @return array
*/
private function fetchMethodContracts(MethodInvocation $invocation)
{
$allContracts = $this->fetchParentsMethodContracts($invocation);

foreach ($invocation->getMethod()->getAnnotations() as $annotation) {
$annotationClass = \get_class($annotation);

if (\in_array($annotationClass, [Ensure::class, Verify::class, Invariant::class], true)) {
$allContracts[] = $annotation;
}
}

return array_unique($allContracts);
}

/**
* @param MethodInvocation $invocation
* @return array
*/
private function fetchParentsMethodContracts(MethodInvocation $invocation)
{
return $this->methodConditionFetcher->getConditions(
$invocation->getMethod()->getDeclaringClass(),
$invocation->getMethod()->name
);
}

/**
* @param ReflectionClass $class
* @return array
*/
private function fetchClassContracts(ReflectionClass $class)
{
$allContracts = $this->invariantFetcher->getConditions($class);
foreach ($this->reader->getClassAnnotations($class) as $annotation) {
if ($annotation instanceof Invariant) {
$allContracts[] = $annotation;
}
}

return array_unique($allContracts);
}
}
2 changes: 1 addition & 1 deletion src/Aspect/InvariantCheckerAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class InvariantCheckerAspect extends AbstractContractAspect implements Aspect
public function __construct(Reader $reader)
{
parent::__construct($reader);
$this->invariantFetcher = new InvariantFetcher(Invariant::class, $reader);
$this->invariantFetcher = new InvariantFetcher([Invariant::class], $reader);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Aspect/PostconditionCheckerAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class PostconditionCheckerAspect extends AbstractContractAspect implements Aspec
public function __construct(Reader $reader)
{
parent::__construct($reader);
$this->methodConditionFetcher = new MethodConditionFetcher(Ensure::class, $reader);
$this->methodConditionFetcher = new MethodConditionFetcher([Ensure::class], $reader);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Aspect/PreconditionCheckerAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class PreconditionCheckerAspect extends AbstractContractAspect implements Aspect
public function __construct(Reader $reader)
{
parent::__construct($reader);
$this->methodConditionFetcher = new MethodConditionWithInheritDocFetcher(Verify::class, $reader);
$this->methodConditionFetcher = new MethodConditionWithInheritDocFetcher([Verify::class], $reader);
}

/**
Expand Down
Loading