' . $valueHolder->getName(); } $originalClassReflection = $originalClass === null ? 'new \\ReflectionClass(get_parent_class($this))' : 'new \\ReflectionClass(' . var_export($originalClass->getName(), true) . ')'; $accessorEvaluation = $returnPropertyName ? '$' . $returnPropertyName . ' = ' . $byRef . '$accessor();' : '$returnValue = ' . $byRef . '$accessor();' . "\n\n" . 'return $returnValue;'; if ($operationType === self::OPERATION_UNSET) { $accessorEvaluation = '$accessor();'; } return '$realInstanceReflection = ' . $originalClassReflection . ';' . "\n\n" . 'if (! $realInstanceReflection->hasProperty($' . $nameParameter . ')) {' . "\n" . ' $targetObject = ' . $target . ';' . "\n\n" . self::getUndefinedPropertyNotice($operationType, $nameParameter) . ' ' . self::getOperation($operationType, $nameParameter, $valueParameter) . "\n" . '}' . "\n\n" . '$targetObject = ' . self::getTargetObject($valueHolder) . ";\n" . '$accessor = function ' . $byRef . '() use (' . implode(', ', array_map( static function (string $parameterName): string { return '$' . $parameterName; }, array_filter(['targetObject', $nameParameter, $valueParameter]) )) . ') {' . "\n" . ' ' . self::getOperation($operationType, $nameParameter, $valueParameter) . "\n" . "};\n" . self::generateScopeReBind() . $accessorEvaluation; } /** * This will generate code that triggers a notice if access is attempted on a non-existing property * * @psalm-param $operationType self::OPERATION_* */ private static function getUndefinedPropertyNotice(string $operationType, string $nameParameter, ?string $interfaceName = null): string { if ($operationType !== self::OPERATION_GET) { return ''; } $code = ' $backtrace = debug_backtrace(false, 1);' . "\n" . ' trigger_error(' . "\n" . ' sprintf(' . "\n" . ' \'Undefined property: %s::$%s in %s on line %s\',' . "\n" . ' $realInstanceReflection->getName(),' . "\n" . ' $' . $nameParameter . ',' . "\n" . ' $backtrace[0][\'file\'],' . "\n" . ' $backtrace[0][\'line\']' . "\n" . ' ),' . "\n" . ' \E_USER_NOTICE' . "\n" . ' );' . "\n"; if ($interfaceName !== null) { $code = str_replace("\n ", "\n", substr($code, 4)); } return $code; } /** * Defines whether the given operation produces a reference. * * Note: if the object is a wrapper, the wrapped instance is accessed directly. If the object * is a ghost or the proxy has no wrapper, then an instance of the parent class is created via * on-the-fly unserialization * * @psalm-param $operationType self::OPERATION_* */ private static function getByRefReturnValue(string $operationType): string { return $operationType === self::OPERATION_GET || $operationType === self::OPERATION_SET ? '& ' : ''; } /** * Retrieves the logic to fetch the object on which access should be attempted */ private static function getTargetObject(?PropertyGenerator $valueHolder = null): string { if ($valueHolder) { return '$this->' . $valueHolder->getName(); } return '$realInstanceReflection->newInstanceWithoutConstructor()'; } /** * @psalm-param $operationType self::OPERATION_* * * @throws InvalidArgumentException */ private static function getOperation(string $operationType, string $nameParameter, ?string $valueParameter): string { if ($valueParameter !== null && $operationType !== self::OPERATION_SET) { throw new InvalidArgumentException( 'Parameter $valueParameter should be provided (only) when $operationType === "' . self::OPERATION_SET . '"' . self::class . '::OPERATION_SET' ); } switch ($operationType) { case self::OPERATION_GET: return 'return $targetObject->$' . $nameParameter . ';'; case self::OPERATION_SET: if ($valueParameter === null) { throw new InvalidArgumentException( 'Parameter $valueParameter should be provided (only) when $operationType === "' . self::OPERATION_SET . '"' . self::class . '::OPERATION_SET' ); } return '$targetObject->$' . $nameParameter . ' = $' . $valueParameter . ';' . "\n\n" . ' return $targetObject->$' . $nameParameter . ';'; case self::OPERATION_ISSET: return 'return isset($targetObject->$' . $nameParameter . ');'; case self::OPERATION_UNSET: return 'unset($targetObject->$' . $nameParameter . ');' . "\n\n" . ' return;'; } throw new InvalidArgumentException(sprintf('Invalid operation "%s" provided', $operationType)); } /** * Generates code to bind operations to the parent scope */ private static function generateScopeReBind(): string { return <<<'PHP' $backtrace = debug_backtrace(true, 2); $scopeObject = isset($backtrace[1]['object']) ? $backtrace[1]['object'] : new \ProxyManager\Stub\EmptyClassStub(); $accessor = $accessor->bindTo($scopeObject, get_class($scopeObject)); PHP; } }