Completed
Push — master ( 83ac96...4c5be0 )
by Alexander
02:42
created

ContractCheckerAspect::getMethodArguments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 10
ccs 7
cts 7
cp 1
rs 9.4285
cc 1
eloc 7
nc 1
nop 1
crap 1
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
11
namespace PhpDeal\Aspect;
12
13
use Doctrine\Common\Annotations\Annotation;
14
use Doctrine\Common\Annotations\Reader;
15
use Go\Aop\Aspect;
16
use Go\Aop\Intercept\MethodInvocation;
17
use Go\Lang\Annotation\Around;
18
use Go\Lang\Annotation\Before;
19
use PhpDeal\Annotation as Contract;
20
use PhpDeal\Exception\ContractViolation;
21
use DomainException;
22
23
/**
24
 */
25
class ContractCheckerAspect implements Aspect
26
{
27
28
    /**
29
     * Annotation reader
30
     *
31
     * @var Reader|null
32
     */
33
    private $reader = null;
34
35
    /**
36
     * Default constructor
37
     *
38
     * @param Reader $reader Annotation reader
39
     */
40
    public function __construct(Reader $reader)
41
    {
42
        $this->reader = $reader;
43
    }
44
45
    /**
46
     * Verifies pre-condition contract for the method
47
     *
48
     * @param MethodInvocation $invocation
49
     * @Before("@execution(PhpDeal\Annotation\Verify)")
50
     *
51
     * @throws ContractViolation
52
     */
53 8
    public function preConditionContract(MethodInvocation $invocation)
54
    {
55 8
        $object = $invocation->getThis();
56 8
        $args   = $this->getMethodArguments($invocation);
57 8
        $scope  = $invocation->getMethod()->getDeclaringClass()->name;
58
59 8
        foreach ($invocation->getMethod()->getAnnotations() as $annotation) {
60 8
            if (!$annotation instanceof Contract\Verify) {
61 1
                continue;
62
            }
63
64
            try {
65 8
                $this->ensureContractSatisfied($object, $scope, $args, $annotation);
66 5
            } catch (\Exception $e) {
67 8
                throw new ContractViolation($invocation, $annotation->value, $e);
68
            }
69
        }
70 3
    }
71
72
    /**
73
     * Verifies post-condition contract for the method
74
     *
75
     * @Around("@execution(PhpDeal\Annotation\Ensure)")
76
     * @param MethodInvocation $invocation
77
     *
78
     * @throws ContractViolation
79
     * @return mixed
80
     */
81 3
    public function postConditionContract(MethodInvocation $invocation)
82
    {
83 3
        $object = $invocation->getThis();
84 3
        $args   = $this->getMethodArguments($invocation);
85 3
        $class  = $invocation->getMethod()->getDeclaringClass();
86 3
        if ($class->isCloneable()) {
87 3
            $args['__old'] = clone $object;
88
        }
89
90 3
        $result = $invocation->proceed();
91 3
        $args['__result'] = $result;
92
93 3
        foreach ($invocation->getMethod()->getAnnotations() as $annotation) {
94 3
            if (!$annotation instanceof Contract\Ensure) {
95
                continue;
96
            }
97
98
            try {
99 3
                $this->ensureContractSatisfied($object, $class->name, $args, $annotation);
100 1
            } catch (\Exception $e) {
101 3
                throw new ContractViolation($invocation, $annotation->value, $e);
102
            }
103
        }
104
105 2
        return $result;
106
    }
107
108
    /**
109
     * Verifies invariants for contract class
110
     *
111
     * @Around("@within(PhpDeal\Annotation\Invariant) && execution(public **->*(*))")
112
     * @param MethodInvocation $invocation
113
     *
114
     * @throws ContractViolation
115
     * @return mixed
116
     */
117 3
    public function invariantContract(MethodInvocation $invocation)
118
    {
119 3
        $object = $invocation->getThis();
120 3
        $args   = $this->getMethodArguments($invocation);
121 3
        $class  = $invocation->getMethod()->getDeclaringClass();
122 3
        if ($class->isCloneable()) {
123 3
            $args['__old'] = clone $object;
124
        }
125
126 3
        $result = $invocation->proceed();
127 3
        $args['__result'] = $result;
128
129 3
        foreach ($this->reader->getClassAnnotations($class) as $annotation) {
130 3
            if (!$annotation instanceof Contract\Invariant) {
131 2
                continue;
132
            }
133
134
            try {
135 3
                $this->ensureContractSatisfied($object, $class->name, $args, $annotation);
136 2
            } catch (\Exception $e) {
137 3
                throw new ContractViolation($invocation, $annotation->value, $e);
138
            }
139
        }
140
141 2
        return $result;
142
    }
143
144
    /**
145
     * Returns a result of contract verification
146
     *
147
     * @param object|string $instance Invocation instance or string for static class
148
     * @param string $scope Scope of method
149
     * @param array $args List of arguments for the method
150
     * @param Annotation $annotation Contract annotation
151
     * @throws DomainException
152
     */
153 14
    private function ensureContractSatisfied($instance, $scope, array $args, $annotation)
154
    {
155 14
        static $invoker = null;
156 14
        if (!$invoker) {
157
            $invoker = function () {
158 14
                extract(func_get_arg(0));
0 ignored issues
show
Bug introduced by
func_get_arg(0) cannot be passed to extract() as the parameter $var_array expects a reference.
Loading history...
159
160 14
                return eval('return ' . func_get_arg(1) . '; ?>');
161 1
            };
162
        }
163 14
        $instance = is_object($instance) ? $instance : null;
164
165 14
        $invocationResult = $invoker->bindTo($instance, $scope)->__invoke($args, $annotation->value);
166
167
        // we accept as a result only true or null
168
        // null may be a result of assertions from beberlei/assert which passed
169 10
        if ($invocationResult !== null && $invocationResult !== true) {
170 4
            $errorMessage = 'Invalid return value received from the assertion body, only boolean or void accepted';
171 4
            throw new DomainException($errorMessage);
172
        }
173 7
    }
174
175
    /**
176
     * Returns an associative list of arguments for the method invocation
177
     *
178
     * @param MethodInvocation $invocation
179
     *
180
     * @return array
181
     */
182 14
    private function getMethodArguments(MethodInvocation $invocation)
183
    {
184 14
        $parameters    = $invocation->getMethod()->getParameters();
185 14
        $argumentNames = array_map(function (\ReflectionParameter $parameter) {
186 13
            return $parameter->name;
187 14
        }, $parameters);
188 14
        $parameters    = array_combine($argumentNames, $invocation->getArguments());
189
190 14
        return $parameters;
191
    }
192
}
193