PublicScopeSimulator::getTargetObject()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
c 0
b 0
f 0
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ProxyManager\ProxyGenerator\Util;
6
7
use InvalidArgumentException;
8
use Laminas\Code\Generator\PropertyGenerator;
9
use function sprintf;
10
11
/**
12
 * Generates code necessary to simulate a fatal error in case of unauthorized
13
 * access to class members in magic methods even when in child classes and dealing
14
 * with protected members.
15
 */
16
class PublicScopeSimulator
17
{
18
    public const OPERATION_SET   = 'set';
19
    public const OPERATION_GET   = 'get';
20
    public const OPERATION_ISSET = 'isset';
21
    public const OPERATION_UNSET = 'unset';
22
23
    /**
24
     * Generates code for simulating access to a property from the scope that is accessing a proxy.
25
     * This is done by introspecting `debug_backtrace()` and then binding a closure to the scope
26
     * of the parent caller.
27
     *
28
     * @param string            $operationType      operation to execute: one of 'get', 'set', 'isset' or 'unset'
29
     * @param string            $nameParameter      name of the `name` parameter of the magic method
30
     * @param string|null       $valueParameter     name of the `value` parameter of the magic method
31
     * @param PropertyGenerator $valueHolder        name of the property containing the target object from which
32
     *                                              to read the property. `$this` if none provided
33
     * @param string|null       $returnPropertyName name of the property to which we want to assign the result of
34
     *                                              the operation. Return directly if none provided
35
     *
36
     * @throws InvalidArgumentException
37
     */
38 8
    public static function getPublicAccessSimulationCode(
39
        string $operationType,
40
        string $nameParameter,
41
        ?string $valueParameter = null,
42
        ?PropertyGenerator $valueHolder = null,
43
        ?string $returnPropertyName = null
44
    ) : string {
45 8
        $byRef  = self::getByRefReturnValue($operationType);
46 8
        $value  = $operationType === self::OPERATION_SET ? ', $value' : '';
47 8
        $target = '$this';
48
49 8
        if ($valueHolder) {
50 1
            $target = '$this->' . $valueHolder->getName();
51
        }
52
53
        return '$realInstanceReflection = new \\ReflectionClass(get_parent_class($this));' . "\n\n"
54 8
            . 'if (! $realInstanceReflection->hasProperty($' . $nameParameter . ')) {' . "\n"
55 8
            . '    $targetObject = ' . $target . ';' . "\n\n"
56 8
            . self::getUndefinedPropertyNotice($operationType, $nameParameter)
57 8
            . '    ' . self::getOperation($operationType, $nameParameter, $valueParameter) . "\n"
58 6
            . "    return;\n"
59 6
            . '}' . "\n\n"
60 6
            . '$targetObject = ' . self::getTargetObject($valueHolder) . ";\n"
61 6
            . '$accessor = function ' . $byRef . '() use ($targetObject, $name' . $value . ') {' . "\n"
62 6
            . '    ' . self::getOperation($operationType, $nameParameter, $valueParameter) . "\n"
63 6
            . "};\n"
64 6
            . self::getScopeReBind()
65
            . (
66 6
                $returnPropertyName
67 5
                    ? '$' . $returnPropertyName . ' = ' . $byRef . '$accessor();'
68 6
                    : '$returnValue = ' . $byRef . '$accessor();' . "\n\n" . 'return $returnValue;'
69
            );
70
    }
71
72
    /**
73
     * This will generate code that triggers a notice if access is attempted on a non-existing property
74
     */
75
    private static function getUndefinedPropertyNotice(string $operationType, string $nameParameter) : string
76
    {
77 8
        if ($operationType !== self::OPERATION_GET) {
78
            return '';
79 8
        }
80 6
81
        return '    $backtrace = debug_backtrace(false);' . "\n"
82
            . '    trigger_error(' . "\n"
83
            . '        sprintf(' . "\n"
84
            . '            \'Undefined property: %s::$%s in %s on line %s\',' . "\n"
85
            . '            get_parent_class($this),' . "\n"
86
            . '            $' . $nameParameter . ',' . "\n"
87
            . '            $backtrace[0][\'file\'],' . "\n"
88 2
            . '            $backtrace[0][\'line\']' . "\n"
89 2
            . '        ),' . "\n"
90 2
            . '        \E_USER_NOTICE' . "\n"
91 2
            . '    );' . "\n";
92 2
    }
93 2
94
    /**
95
     * Defines whether the given operation produces a reference.
96
     *
97
     * Note: if the object is a wrapper, the wrapped instance is accessed directly. If the object
98
     * is a ghost or the proxy has no wrapper, then an instance of the parent class is created via
99
     * on-the-fly unserialization
100
     */
101
    private static function getByRefReturnValue(string $operationType) : string
102
    {
103 8
        return $operationType === self::OPERATION_GET || $operationType === self::OPERATION_SET ? '& ' : '';
104
    }
105 8
106
    /**
107
     * Retrieves the logic to fetch the object on which access should be attempted
108
     */
109
    private static function getTargetObject(?PropertyGenerator $valueHolder = null) : string
110
    {
111
        if ($valueHolder) {
112
            return '$this->' . $valueHolder->getName();
113 6
        }
114
115 6
        return '$realInstanceReflection->newInstanceWithoutConstructor()';
116 1
    }
117
118
    /**
119 5
     * @throws InvalidArgumentException
120
     */
121
    private static function getOperation(string $operationType, string $nameParameter, ?string $valueParameter) : string
122
    {
123
        switch ($operationType) {
124
            case self::OPERATION_GET:
125 8
                return 'return $targetObject->$' . $nameParameter . ';';
126
            case self::OPERATION_SET:
127
                if ($valueParameter === null) {
128 8
                    throw new InvalidArgumentException('Parameter $valueParameter not provided');
129 2
                }
130 6
131 3
                return 'return $targetObject->$' . $nameParameter . ' = $' . $valueParameter . ';';
132 1
            case self::OPERATION_ISSET:
133
                return 'return isset($targetObject->$' . $nameParameter . ');';
134
            case self::OPERATION_UNSET:
135 2
                return 'unset($targetObject->$' . $nameParameter . ');';
136 3
        }
137 1
138 2
        throw new InvalidArgumentException(sprintf('Invalid operation "%s" provided', $operationType));
139 1
    }
140
141
    /**
142 1
     * Generates code to bind operations to the parent scope
143
     */
144
    private static function getScopeReBind() : string
145
    {
146
        return '$backtrace = debug_backtrace(true);' . "\n"
147
            . '$scopeObject = isset($backtrace[1][\'object\'])'
148
            . ' ? $backtrace[1][\'object\'] : new \ProxyManager\Stub\EmptyClassStub();' . "\n"
149 6
            . '$accessor = $accessor->bindTo($scopeObject, get_class($scopeObject));' . "\n";
150
    }
151
}
152