├── .gitignore
├── phpbridge
├── php-server
│ ├── Exceptions
│ │ ├── AttributeError.php
│ │ └── ConnectionLostException.php
│ ├── Representer
│ │ ├── RepresenterInterface.php
│ │ ├── PythonRepresenter.php
│ │ └── Representer.php
│ ├── ObjectStore.php
│ ├── StdioCommandServer.php
│ ├── NonFunctionProxy.php
│ ├── CommandServer.php
│ └── Commands.php
├── server.php
├── utils.py
├── docblocks.py
├── modules.py
├── classes.py
├── functions.py
├── objects.py
└── __init__.py
├── Makefile
├── examples
└── doctrine.py
├── LICENSE
├── composer.json
├── TODO.org
├── setup.py
├── psalm.xml
├── README.md
└── composer.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.pyc
3 | /.mypy_cache
4 | /vendor
5 | /.idea
6 | /phpbridge.egg-info
7 | /build
8 | /dist
9 |
--------------------------------------------------------------------------------
/phpbridge/php-server/Exceptions/AttributeError.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | Permission to use, copy, modify, and distribute this software for any
4 | purpose with or without fee is hereby granted, provided that the above
5 | copyright notice and this permission notice appear in all copies.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blyxxyz/python-server",
3 | "description": "A way to call PHP code from Python",
4 | "type": "library",
5 | "keywords": ["python"],
6 | "homepage": "https://github.com/blyxxyz/Python-PHP-Bridge",
7 | "license": "ISC",
8 | "require-dev": {
9 | "vimeo/psalm": "^1.1",
10 | "squizlabs/php_codesniffer": "^3.2"
11 | },
12 | "authors": [
13 | {
14 | "name": "Jan Verbeek",
15 | "email": "jan.verbeek@posteo.nl"
16 | }
17 | ],
18 | "require": {
19 | "php": "^7.0",
20 | "ext-json": "*",
21 | "ext-Reflection": "*"
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "blyxxyz\\PythonServer\\": "phpbridge/php-server"
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/TODO.org:
--------------------------------------------------------------------------------
1 | - Convert use of send_command to bridge methods
2 | - Change phpbridge.classes to use inherited fake traits
3 | - Add a helpers submodule for things like autoloading and Symfony
4 | - If composer dump-autoload -o is used, namespaces could be imported in advance and importing new ones could be disabled after that
5 | - This would help with some nasty edge cases in the current system
6 | - A clean way to get Symfony services
7 | - Document more
8 | - Don't let bridge namespace modules conflict with other modules
9 | - Publish to PyPi
10 | - Write tests
11 | - Don't use stdin and stderr on Windows, if possible
12 | - Make bridge more symmetric
13 | - Allow sending Python proxy objects into PHP
14 | - Allow Python to receive commands from PHP
15 | - Try to make it possible to recover from a missed message
16 | - Try to deal with KeyboardInterrupts simultaneously killing PHP
17 | - Try to auto-generate mypy stubs
18 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", 'r') as f:
4 | long_description = f.read()
5 |
6 | setuptools.setup(
7 | name="phpbridge",
8 | version="0.0.3",
9 | author="Jan Verbeek",
10 | author_email="jan.verbeek@posteo.nl",
11 | description="Import PHP code into Python",
12 | long_description=long_description,
13 | long_description_content_type="text/markdown",
14 | url="https://github.com/blyxxyz/Python-PHP-Bridge",
15 | packages=setuptools.find_packages(),
16 | package_data={
17 | 'phpbridge': ['*.php', '*/*.php', '*/*/*.php', '*/*/*/*.php'],
18 | },
19 | license="ISC",
20 | classifiers=[
21 | "Programming Language :: PHP",
22 | "Programming Language :: Python :: 3",
23 | "Programming Language :: Python :: 3",
24 | "Programming Language :: Python :: 3 :: Only",
25 | "Programming Language :: Python :: 3.5",
26 | "License :: OSI Approved :: ISC License (ISCL)",
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/phpbridge/php-server/Representer/PythonRepresenter.php:
--------------------------------------------------------------------------------
1 | module = $module;
17 | }
18 |
19 | const TRUE = 'True';
20 | const FALSE = 'False';
21 | const NULL = 'None';
22 |
23 | const OBJECT_IDEN = 'PHP object';
24 | const RESOURCE_IDEN = 'PHP resource';
25 |
26 | const SEQ_ARRAY_DELIMS = ['[', ']'];
27 | const ASSOC_ARRAY_DELIMS = ['{', '}'];
28 | const KEY_SEP = ': ';
29 | const ITEM_SEP = ', ';
30 |
31 | const NAN = 'nan';
32 | const INF = 'inf';
33 | const NEG_INF = '-inf';
34 |
35 | // Overriding reprString would be good for correctness but a lot of work
36 |
37 | protected function convertClassName(string $name): string
38 | {
39 | $repr = str_replace('\\', '.', $name);
40 | if ($this->module !== '') {
41 | $repr = $this->module . '.' . $repr;
42 | }
43 | return $repr;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/phpbridge/server.php:
--------------------------------------------------------------------------------
1 | communicate();
46 | } catch (\blyxxyz\PythonServer\Exceptions\ConnectionLostException $exception) {
47 | }
48 |
--------------------------------------------------------------------------------
/phpbridge/php-server/ObjectStore.php:
--------------------------------------------------------------------------------
1 | */
12 | private $objects;
13 |
14 | /** @var array */
15 | private $resources;
16 |
17 | public function __construct()
18 | {
19 | $this->objects = [];
20 | $this->resources = [];
21 | }
22 |
23 | /**
24 | * @param object|resource $object
25 | *
26 | * @return string|int
27 | */
28 | public function encode($object)
29 | {
30 | if (is_resource($object)) {
31 | // This uses an implementation detail, but it's the best we have
32 | $key = intval($object);
33 | $this->resources[$key] = $object;
34 | } else {
35 | $key = spl_object_hash($object);
36 | $this->objects[$key] = $object;
37 | }
38 | return $key;
39 | }
40 |
41 | /**
42 | * @param string|int $key
43 | *
44 | * @return object|resource
45 | */
46 | public function decode($key)
47 | {
48 | if (is_int($key)) {
49 | return $this->resources[$key];
50 | } else {
51 | return $this->objects[$key];
52 | }
53 | }
54 |
55 | /**
56 | * @param string|int $key
57 | *
58 | * @return void
59 | */
60 | public function remove($key)
61 | {
62 | if (is_int($key)) {
63 | unset($this->resources[$key]);
64 | } else {
65 | unset($this->objects[$key]);
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/phpbridge/utils.py:
--------------------------------------------------------------------------------
1 | """Stand-alone utility functions."""
2 |
3 | from inspect import Parameter, Signature
4 | from typing import Any, Dict, Optional, Tuple
5 |
6 |
7 | def convert_docblock(doc: Optional[str]) -> Optional[str]:
8 | """Strip the comment syntax out of a docblock."""
9 | if not isinstance(doc, str):
10 | return doc
11 | doc = doc.strip('/*')
12 | lines = doc.split('\n')
13 | lines = [line.strip() for line in lines]
14 | lines = [line[1:] if line.startswith('*') else line
15 | for line in lines]
16 | return '\n'.join(lines)
17 |
18 |
19 | def parse_args(signature: Signature, orig_args: Tuple,
20 | orig_kwargs: Dict[str, Any]) -> Tuple:
21 | """Use a function signature to interpret keyword arguments.
22 |
23 | If no keyword arguments are provided, args is always returned unchanged.
24 | This ensures that it's possible to call a function even if the signature
25 | is inaccurate. inspect.Signature.bind is similar, but less forgiving.
26 | """
27 | if not orig_kwargs:
28 | return orig_args
29 | args = list(orig_args)
30 | kwargs = dict(orig_kwargs)
31 | parameters = list(signature.parameters.values())
32 | while kwargs:
33 | index = len(args)
34 | if index >= len(parameters):
35 | key, value = kwargs.popitem()
36 | raise TypeError("Can't handle keyword argument '{}'".format(key))
37 | cur_param = parameters[index]
38 | name = cur_param.name
39 | default = cur_param.default
40 | if default is unknown_param_default:
41 | raise TypeError("Missing value for argument '{}' with "
42 | "unknown default".format(name))
43 | if name in kwargs:
44 | args.append(kwargs.pop(name))
45 | else:
46 | if default == Parameter.empty:
47 | raise TypeError("Missing required argument '{}'".format(name))
48 | args.append(default)
49 | return tuple(args)
50 |
51 |
52 | class _UnknownParameterDefaultValue:
53 | """Represent optional parameters without known defaults."""
54 | def __repr__(self) -> str:
55 | return '?'
56 |
57 |
58 | unknown_param_default = _UnknownParameterDefaultValue()
59 |
--------------------------------------------------------------------------------
/phpbridge/php-server/StdioCommandServer.php:
--------------------------------------------------------------------------------
1 | in = fopen($in, 'r');
26 | $this->out = fopen($out, 'w');
27 | }
28 |
29 | public function receive(): array
30 | {
31 | $line = fgets($this->in);
32 | if ($line === false) {
33 | throw new ConnectionLostException("Can't read from input");
34 | }
35 | $result = json_decode($line, true);
36 | if (!is_array($result)) {
37 | return [
38 | 'cmd' => 'throwException',
39 | 'data' => [
40 | 'class' => \RuntimeException::class,
41 | 'message' => "Error decoding JSON: " . json_last_error_msg()
42 | ]
43 | ];
44 | }
45 | return $result;
46 | }
47 |
48 | public function send(array $data)
49 | {
50 | $encoded = json_encode($data, JSON_PRESERVE_ZERO_FRACTION);
51 | if ($encoded === false) {
52 | $encoded = json_encode($this->encodeThrownException(
53 | new \RuntimeException(json_last_error_msg()),
54 | $data['collected']
55 | ));
56 | }
57 | fwrite($this->out, $encoded);
58 | fwrite($this->out, "\n");
59 | }
60 |
61 | /**
62 | * Promote warnings to exceptions. This is vital if stderr is used for
63 | * communication, because anything else written there will then disrupt
64 | * the connection.
65 | *
66 | * @return mixed
67 | */
68 | public static function promoteWarnings()
69 | {
70 | return set_error_handler(function (int $errno, string $errstr): bool {
71 | if (error_reporting() !== 0) {
72 | throw new \ErrorException($errstr);
73 | }
74 | return false;
75 | });
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/phpbridge/docblocks.py:
--------------------------------------------------------------------------------
1 | """A module for parsing docblock types into mypy-style types.
2 |
3 | This was originally going to be used to enhance the function signatures, but
4 | it's useful to know the types that PHP will actually check for and the docblock
5 | types are visible in the __doc__ anyway so it's not used at all now.
6 |
7 | It could still be useful for auto-generating mypy stubs, or something.
8 | """
9 |
10 | from typing import Any, Callable, Dict, Optional, Sequence, Type, Union
11 |
12 | import phpbridge
13 | import re
14 |
15 |
16 | def parse_type(bridge: 'phpbridge.PHPBridge',
17 | spec: str,
18 | cls: Optional[Type] = None,
19 | bases: Optional[Sequence[Type]] = None) -> Any:
20 | """Try to parse a PHPDoc type specification.
21 |
22 | Mostly follows https://docs.phpdoc.org/guides/types.html.
23 | """
24 |
25 | if not all(char.isalnum() or char in '|[]?\\'
26 | for char in spec):
27 | # The spec is probably too advanced for us to deal with properly
28 | # In that case, return a string
29 | # This excludes some valid class names, but if you're using
30 | # non-alphanumeric unicode in your class names, please stop
31 | return spec
32 |
33 | simple_types = {
34 | 'string': str,
35 | 'int': int,
36 | 'integer': int,
37 | 'float': float,
38 | 'double': float, # This is not official but probably better
39 | 'bool': bool,
40 | 'boolean': bool,
41 | 'array': dict,
42 | 'resource': phpbridge.objects.PHPResource,
43 | 'object': phpbridge.objects.PHPObject,
44 | 'null': None,
45 | 'void': None,
46 | 'callable': Callable,
47 | 'mixed': Any,
48 | 'false': False,
49 | 'true': True,
50 | 'self': cls if cls is not None else 'self',
51 | 'static': 'static', # These are tricky, maybe work them out later
52 | '$this': '$this'
53 | }
54 |
55 | parsed = []
56 |
57 | for option in spec.split('|'): # type: Any
58 |
59 | is_array = False
60 | if option.endswith('[]'):
61 | option = option[:-2]
62 | is_array = True
63 |
64 | is_optional = False
65 | if option.startswith('?'):
66 | option = option[1:]
67 | is_optional = True
68 |
69 | if any(char in option for char in '[]?'):
70 | # This is too advanced, abort
71 | return spec
72 |
73 | if option in simple_types:
74 | option = simple_types[option]
75 | else:
76 | try:
77 | option = bridge.get_class(option)
78 | except Exception:
79 | # Give up and keep it as a string
80 | pass
81 |
82 | if is_array:
83 | option = Dict[Union[int, str], option]
84 |
85 | if is_optional:
86 | option = Optional[option]
87 |
88 | parsed.append(option)
89 |
90 | if parsed != [None]:
91 | # A Union with one member just becomes that member so this is ok
92 | return Union[tuple(parsed)]
93 | else:
94 | # But Union[None] becomes NoneType and that's ugly
95 | return None
96 |
97 |
98 | def get_signature(bridge: 'phpbridge.PHPBridge',
99 | docblock: str,
100 | cls: Optional[Type] = None,
101 | bases: Optional[Sequence[Type]] = None):
102 | params = {name: parse_type(bridge, spec, cls, bases)
103 | for spec, name in
104 | re.findall(r'\@param\s+([^\s]*)\s+\$([^\s\*]+)', docblock)}
105 | match = re.search(r'\@return\s+([^\s]*)', docblock)
106 | if match is not None:
107 | ret = parse_type(bridge, match.group(1), cls, bases)
108 | else:
109 | # "None" would be a valid return type, so we use this
110 | ret = ''
111 | return params, ret
112 |
--------------------------------------------------------------------------------
/phpbridge/php-server/NonFunctionProxy.php:
--------------------------------------------------------------------------------
1 | None:
22 | super().__init__(name, doc)
23 | self._bridge = bridge
24 | self._path = path
25 | self.__path__ = [] # type: List[str]
26 |
27 | def __getattribute__(self, attr: str) -> Any:
28 | val = None
29 | try:
30 | val = super().__getattribute__(attr)
31 | if not isinstance(val, Namespace):
32 | return val
33 | except AttributeError:
34 | pass
35 | try:
36 | path = super().__getattribute__('_path')
37 | return self._bridge.resolve(path, attr)
38 | except AttributeError:
39 | if val is not None:
40 | # We did manage to get val, but it's a Namespace
41 | # It's ok as a fallback
42 | return val
43 | raise
44 |
45 | def __dir__(self) -> Generator:
46 | yield from super().__dir__()
47 | for entry in self._bridge.send_command('listEverything', self._path):
48 | if '\\' not in entry:
49 | yield entry
50 | continue
51 |
52 | @property
53 | def __all__(self) -> List[str]:
54 | """This is unreliable because of autoloading."""
55 | return list(dir(self))
56 |
57 | def __repr__(self) -> str:
58 | if self._path:
59 | return "".format(self._path)
60 | return ""
61 |
62 | def __getitem__(self, index: str) -> Any:
63 | try:
64 | path = super().__getattribute__('_path')
65 | return self._bridge.resolve(path, index)
66 | except AttributeError as e:
67 | raise KeyError(*e.args)
68 |
69 |
70 | class NamespaceLoader(importlib.abc.Loader):
71 | def __init__(self, bridge: 'PHPBridge', path: str) -> None:
72 | self.bridge = bridge
73 | self.path = path
74 |
75 | def create_module(self, spec: ModuleSpec) -> Namespace:
76 | return Namespace(spec.name, bridge=self.bridge, path=self.path)
77 |
78 | def exec_module(self, module: ModuleType) -> None:
79 | pass
80 |
81 |
82 | class NamespaceFinder(importlib.abc.MetaPathFinder):
83 | """Import PHP namespaces over a bridge."""
84 | def __init__(self, bridge: Union['PHPBridge', Callable[[], 'PHPBridge']],
85 | name: str) -> None:
86 | self.bridge = bridge
87 | self.prefix = __package__ + '.' + name
88 | if isinstance(self.bridge, phpbridge.PHPBridge):
89 | bridges[self.bridge] = self.prefix
90 |
91 | def resolve(self, name: str) -> Optional[str]:
92 | """Parse a module name into a namespace identity.
93 |
94 | Args:
95 | - name: The prospective module path.
96 |
97 | Return:
98 | - None if the name doesn't specify a namespace.
99 | - A string representing the namespace otherwise.
100 | """
101 | if name == self.prefix:
102 | return ''
103 |
104 | if not name.startswith(self.prefix + '.'):
105 | return None
106 |
107 | name = name[len(self.prefix) + 1:]
108 | return name.replace('.', '\\')
109 |
110 | def find_spec(self, fullname: str, path: Any,
111 | target: Optional[ModuleType] = None) -> Optional[ModuleSpec]:
112 | namespace = self.resolve(fullname)
113 | if namespace is None:
114 | return None
115 | if not isinstance(self.bridge, phpbridge.PHPBridge):
116 | # Lazy bridge
117 | self.bridge = self.bridge()
118 | bridges[self.bridge] = self.prefix
119 | loader = NamespaceLoader(self.bridge, namespace)
120 | return ModuleSpec(fullname, loader, is_package=True)
121 |
122 | def register(self) -> None:
123 | """Add self to sys.meta_path."""
124 | if self not in sys.meta_path:
125 | sys.meta_path.append(self)
126 |
127 |
128 | def get_module(bridge: 'PHPBridge', name: str) -> str:
129 | """Get the full name of the module that contains a name's namespace."""
130 | prefix = bridges[bridge]
131 | namespace, _, name = name.rpartition('\\')
132 | if namespace == '':
133 | module = prefix
134 | else:
135 | module = prefix + '.' + namespace.replace('\\', '.')
136 | if module not in sys.modules:
137 | importlib.import_module(module)
138 | return module
139 |
140 |
141 | def basename(fullyqualname: str) -> str:
142 | """Remove a name's namespace, so it can be used in a __qualname__.
143 |
144 | It may seem wrong that the fully qualified PHP name is used as __name__,
145 | and the unqualified PHP name is used as __qualname__, but Python's
146 | qualified names shouldn't include the module name.
147 | """
148 | return fullyqualname.rpartition('\\')[2]
149 |
--------------------------------------------------------------------------------
/phpbridge/classes.py:
--------------------------------------------------------------------------------
1 | """PHP classes with special Python behavior.
2 |
3 | These classes either provide special methods or inherit from Python classes.
4 | They are not subclassed or instantiated directly. Instead, their methods and
5 | bases are copied to the corresponding PHPClasses. This helps keep the class
6 | hierarchy clean, because each class must be bound to a bridge.
7 | """
8 |
9 | import typing
10 |
11 | from typing import Any, Callable, Dict, Optional, Type, Union # noqa: F401
12 |
13 | from phpbridge.objects import PHPObject
14 |
15 | predef_classes = {} # type: Dict[str, Type]
16 |
17 | magic_aliases = {
18 | '__toString': '__str__',
19 | '__invoke': '__call__'
20 | } # type: Dict[str, str]
21 |
22 |
23 | def predef(_cls: Optional[Type] = None, *,
24 | name: Optional[str] = None) -> Union[Callable[[Type], Type], Type]:
25 | """A decorator to add a class to the dictionary of pre-defined classes."""
26 |
27 | def decorator(cls: Type) -> Type:
28 | nonlocal name
29 | if name is None:
30 | name = cls.__name__
31 | predef_classes[name] = cls
32 | return cls
33 |
34 | if _cls is None:
35 | return decorator
36 |
37 | return decorator(_cls)
38 |
39 |
40 | @predef
41 | class Countable(PHPObject):
42 | """Classes that can be used with count() (PHP) or len() (Python).
43 |
44 | See also: collections.abc.Sized.
45 | """
46 | def __len__(self) -> int:
47 | return self._bridge.send_command( # type: ignore
48 | 'count', self._bridge.encode(self))
49 |
50 |
51 | @predef
52 | class Iterator(PHPObject):
53 | """Interface for objects that can be iterated.
54 |
55 | See also: collections.abc.Iterator.
56 | """
57 | def __next__(self) -> Any:
58 | status, key, value = self._bridge.send_command(
59 | 'nextIteration', self._bridge.encode(self), decode=True)
60 | if not status:
61 | raise StopIteration
62 | return key, value
63 |
64 |
65 | @predef
66 | class Traversable(PHPObject):
67 | """Interface to detect if a class is traversable using a for(each) loop.
68 |
69 | See also: collections.abc.Iterable.
70 | """
71 | def __iter__(self) -> typing.Iterator:
72 | return self._bridge.send_command( # type: ignore
73 | 'startIteration', self._bridge.encode(self), decode=True)
74 |
75 |
76 | @predef
77 | class ArrayAccess(PHPObject):
78 | """Interface to provide accessing objects as arrays.
79 |
80 | Note that the "in" operator only ever checks for valid keys when it comes
81 | to this class. It's less general than the usual possibilities.
82 | """
83 | def __contains__(self, item: Any) -> bool:
84 | return self._bridge.send_command( # type: ignore
85 | 'hasItem',
86 | {'obj': self._bridge.encode(self),
87 | 'offset': self._bridge.encode(item)})
88 |
89 | def __getitem__(self, item: Any) -> Any:
90 | return self._bridge.send_command(
91 | 'getItem',
92 | {'obj': self._bridge.encode(self),
93 | 'offset': self._bridge.encode(item)},
94 | decode=True)
95 |
96 | def __setitem__(self, item: Any, value: Any) -> None:
97 | self._bridge.send_command(
98 | 'setItem',
99 | {'obj': self._bridge.encode(self),
100 | 'offset': self._bridge.encode(item),
101 | 'value': self._bridge.encode(value)})
102 |
103 | def __delitem__(self, item: Any) -> None:
104 | self._bridge.send_command(
105 | 'delItem',
106 | {'obj': self._bridge.encode(self),
107 | 'offset': self._bridge.encode(item)})
108 |
109 |
110 | @predef
111 | class Throwable(PHPObject, Exception):
112 | """An exception created in PHP.
113 |
114 | Both a valid Exception and a valid PHPObject, so can be raised and
115 | caught.
116 | """
117 | def __init__(self, *args: Any, **kwargs: Any) -> None:
118 | super(Exception, self).__init__(self.getMessage())
119 |
120 |
121 | # Closure is somehow hardwired to be callable, without __invoke
122 | # TODO: It would be nice to generate a signature for this
123 | @predef
124 | class Closure(PHPObject):
125 | def __call__(self, *args: Any) -> Any:
126 | return self._bridge.send_command(
127 | 'callObj',
128 | {'obj': self._bridge.encode(self),
129 | 'args': [self._bridge.encode(arg) for arg in args]},
130 | decode=True)
131 |
132 |
133 | @predef(name='ArithmeticError')
134 | class PHPArithmeticError(PHPObject, ArithmeticError):
135 | pass
136 |
137 |
138 | @predef(name='AssertionError')
139 | class PHPAssertionError(PHPObject, AssertionError):
140 | pass
141 |
142 |
143 | @predef
144 | class OutOfBoundsException(PHPObject, IndexError):
145 | pass
146 |
147 |
148 | @predef
149 | class OverflowException(PHPObject, OverflowError):
150 | pass
151 |
152 |
153 | @predef
154 | class ParseError(PHPObject, SyntaxError):
155 | pass
156 |
157 |
158 | @predef
159 | class RuntimeException(PHPObject, RuntimeError):
160 | pass
161 |
162 |
163 | @predef(name='TypeError')
164 | class PHPTypeError(PHPObject, TypeError):
165 | pass
166 |
167 |
168 | @predef
169 | class UnexpectedValueException(PHPObject, ValueError):
170 | pass
171 |
172 |
173 | @predef(name=r'blyxxyz\PythonServer\Exceptions\AttributeError')
174 | class PHPAttributeError(PHPObject, AttributeError):
175 | pass
176 |
--------------------------------------------------------------------------------
/phpbridge/functions.py:
--------------------------------------------------------------------------------
1 | import itertools
2 |
3 | from inspect import Parameter, Signature
4 | from typing import Any, Callable, Dict, Iterator, Optional, Set # noqa: F401
5 |
6 | from phpbridge import modules, utils
7 |
8 | MYPY = False
9 | if MYPY:
10 | from phpbridge import PHPBridge # noqa: F401
11 |
12 |
13 | def parse_type_info(bridge: 'PHPBridge', info: Dict[str, Any]) -> Any:
14 | """Turn a type info dict into an annotation."""
15 | from phpbridge import objects
16 |
17 | if info['isClass']:
18 | try:
19 | annotation = bridge.get_class(info['name']) # type: Any
20 | except Exception:
21 | # This probably means the type annotation is invalid.
22 | # PHP just lets that happen, because the annotation might start
23 | # existing in the future. So we'll allow it too. But we can't get
24 | # the class, so use the name of the class instead.
25 | annotation = (modules.get_module(bridge, info['name']) + '.' +
26 | modules.basename(info['name']))
27 | else:
28 | annotation = objects.php_types.get(info['name'], info['name'])
29 | if info['nullable']:
30 | annotation = Optional[annotation]
31 |
32 | return annotation
33 |
34 |
35 | def different_name(name: str) -> Iterator[str]:
36 | """Look for new names that don't conflict with existing names."""
37 | yield name
38 | for n in itertools.count(2):
39 | yield name + str(n)
40 |
41 |
42 | def make_signature(bridge: 'PHPBridge', info: Dict[str, Any],
43 | add_first: Optional[str] = None) -> Signature:
44 | """Create a function signature from an info dict."""
45 | parameters = []
46 | used_names = set() # type: Set[str]
47 |
48 | if add_first:
49 | parameters.append(Parameter(
50 | name=add_first,
51 | kind=Parameter.POSITIONAL_OR_KEYWORD,
52 | default=Parameter.empty,
53 | annotation=Parameter.empty))
54 | used_names.add(add_first)
55 |
56 | # Mysteriously enough, PHP lets you make a function with default arguments
57 | # before arguments without a default. Python doesn't. So if that happens,
58 | # we mark it as an unknown default.
59 | # default_required is set to True as soon as we find the first parameter
60 | # with a default value.
61 | default_required = False
62 |
63 | for param in info['params']:
64 |
65 | for param_name in different_name(param['name']):
66 | if param_name not in used_names:
67 | used_names.add(param_name)
68 | break
69 |
70 | default = (bridge.decode(param['default']) if param['hasDefault'] else
71 | Parameter.empty)
72 |
73 | if (param['isOptional'] and not param['variadic'] and
74 | default is Parameter.empty):
75 | # Some methods have optional parameters without (visible) default
76 | # values. We'll use this to represent those.
77 | default = utils.unknown_param_default
78 |
79 | if (default_required and default is Parameter.empty and
80 | not param['variadic']):
81 | default = utils.unknown_param_default
82 |
83 | if default is not Parameter.empty:
84 | # From now on, we need a default value even if we can't find one.
85 | default_required = True
86 |
87 | if param['type'] is None:
88 | annotation = Parameter.empty
89 | else:
90 | annotation = parse_type_info(bridge, param['type'])
91 |
92 | kind = (Parameter.VAR_POSITIONAL if param['variadic'] else
93 | Parameter.POSITIONAL_OR_KEYWORD)
94 |
95 | parameters.append(Parameter(
96 | name=param_name,
97 | kind=kind,
98 | default=default,
99 | annotation=annotation))
100 |
101 | return_annotation = (Signature.empty if info['returnType'] is None else
102 | parse_type_info(bridge, info['returnType']))
103 |
104 | return Signature(parameters=parameters,
105 | return_annotation=return_annotation)
106 |
107 |
108 | default_constructor_signature = Signature(
109 | parameters=[Parameter(name='cls',
110 | kind=Parameter.POSITIONAL_OR_KEYWORD,
111 | default=Parameter.empty,
112 | annotation=Parameter.empty)],
113 | return_annotation=Signature.empty)
114 |
115 |
116 | def create_function(bridge: 'PHPBridge', name: str) -> None:
117 | """Create and register a PHP function."""
118 | info = bridge.send_command('funcInfo', name)
119 |
120 | if info['name'] in bridge.functions:
121 | bridge.functions[name] = bridge.functions[info['name']]
122 | return
123 |
124 | def func(*args: Any, **kwargs: Any) -> Any:
125 | args = utils.parse_args(
126 | func.__signature__, args, kwargs) # type: ignore
127 | return bridge.send_command(
128 | 'callFun',
129 | {'name': name,
130 | 'args': [bridge.encode(arg) for arg in args]},
131 | decode=True)
132 |
133 | func.__doc__ = utils.convert_docblock(info['doc'])
134 | func.__module__ = modules.get_module(bridge, name)
135 | func.__name__ = info['name']
136 | func.__qualname__ = modules.basename(info['name'])
137 | func.__signature__ = make_signature(bridge, info) # type: ignore
138 | func._bridge = bridge # type: ignore
139 |
140 | bridge.functions[name] = func
141 | bridge.functions[info['name']] = func
142 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a Python module for running PHP programs. It lets you import PHP functions, classes, objects, constants and variables to work just like regular Python versions.
2 |
3 | # Examples
4 |
5 | You can call functions:
6 | ```pycon
7 | >>> from phpbridge import php
8 | >>> php.array_reverse(['foo', 'bar', 'baz'])
9 | Array.list(['baz', 'bar', 'foo'])
10 | >>> php.echo("foo\n")
11 | foo
12 | >>> php.getimagesize("http://php.net/images/logos/new-php-logo.png")
13 | Array([('0', 200), ('1', 106), ('2', 3), ('3', 'width="200" height="106"'), ('bits', 8), ('mime', 'image/png')])
14 | ```
15 |
16 | You can create and use objects:
17 | ```pycon
18 | >>> php.DateTime
19 |
20 | >>> date = php.DateTime()
21 | >>> print(date)
22 |
23 | >>> date.getOffset()
24 | 7200
25 | >>> php.ArrayAccess
26 |
27 | >>> issubclass(php.ArrayObject, php.ArrayAccess)
28 | True
29 | ```
30 |
31 | You can use keyword arguments, even though PHP doesn't support them:
32 | ```pycon
33 | >>> date.setDate(year=1900, day=20, month=10)
34 |
35 | ```
36 |
37 | You can loop over iterators and traversables:
38 | ```pycon
39 | >>> for path, file in php.RecursiveIteratorIterator(php.RecursiveDirectoryIterator('.git/logs')):
40 | ... print("{}: {}".format(path, file.getSize()))
41 | ...
42 | .git/logs/.: 16
43 | .git/logs/..: 144
44 | .git/logs/HEAD: 2461
45 | [...]
46 | ```
47 |
48 | You can get help:
49 | ```pycon
50 | >>> help(php.echo)
51 | Help on function echo:
52 |
53 | echo(arg1, *rest)
54 | Output one or more strings.
55 |
56 | @param mixed $arg1
57 | @param mixed ...$rest
58 |
59 | @return void
60 | ```
61 |
62 | You can import namespaces as modules:
63 | ```pycon
64 | >>> from phpbridge.php.blyxxyz.PythonServer import NonFunctionProxy
65 | >>> help(NonFunctionProxy)
66 | Help on class blyxxyz\PythonServer\NonFunctionProxy in module phpbridge.php.blyxxyz.PythonServer:
67 |
68 | class blyxxyz\PythonServer\NonFunctionProxy(phpbridge.objects.PHPObject)
69 | | Provide function-like language constructs as static methods.
70 | |
71 | | `isset` and `empty` are not provided because it's impossible for a real
72 | | function to check whether its argument is defined.
73 | |
74 | | Method resolution order:
75 | | blyxxyz\PythonServer\NonFunctionProxy
76 | | phpbridge.objects.PHPObject
77 | | builtins.object
78 | |
79 | | Class methods defined here:
80 | |
81 | | array(val) -> dict from phpbridge.objects.PHPClass
82 | | Cast a value to an array.
83 | |
84 | | @param mixed $val
85 | |
86 | | @return array
87 | [...]
88 | ```
89 |
90 | You can index, and get lengths:
91 | ```pycon
92 | >>> arr = php.ArrayObject(['foo', 'bar', 'baz'])
93 | >>> arr[10] = 'foobar'
94 | >>> len(arr)
95 | 4
96 | ```
97 |
98 | You can work with PHP's exceptions:
99 | ```pycon
100 | >>> try:
101 | ... php.get_resource_type(3)
102 | ... except php.TypeError as e:
103 | ... print(e.getMessage())
104 | ...
105 | get_resource_type() expects parameter 1 to be resource, integer given
106 | ```
107 |
108 | # Features
109 | * Using PHP functions
110 | * Keyword arguments are supported and translated based on the signature
111 | * Docblocks are also converted, so `help` is informative
112 | * Using PHP classes like Python classes
113 | * Methods and constants are defined right away based on the PHP class
114 | * Docblocks are treated like docstrings, so `help` works and is informative
115 | * The original inheritance structure is copied
116 | * Default properties become Python properties with documentation
117 | * Other properties are accessed on the fly as a fallback for attribute access
118 | * Creating and using objects
119 | * Importing namespaces as modules
120 | * Getting and setting constants
121 | * Getting and setting global variables
122 | * Translating exceptions so they can be treated as both Python exceptions and PHP objects
123 | * Tab completion in the interpreter
124 | * Python-like reprs for PHP objects, with information like var_dump in a more compact form
125 |
126 | # Caveats
127 | * On Windows, stdin and stderr are used to communicate, so PHP can't read input and if it writes to stderr the connection is lost
128 | * You can only pass basic Python objects into PHP
129 | * Namespaces can shadow names in an unintuitive way
130 | * Because PHP only has one kind of array, its arrays are translated to a special kind of ordered dictionary
131 |
132 | # Name conflicts
133 | Some PHP packages use the same name both for a class and a namespace. As an example, take `nikic/PHP-Parser`.
134 |
135 | `PhpParser\Node` is a class, but `PhpParser\Node\Param` is also a class. This means `phpbridge.php.PhpParser.Node` becomes ambiguous - it could either refer to the `Node` class, or the namespace of the `Param` class.
136 |
137 | In case of such a conflict, the class is preferred over the namespace. To get `Param`, a `from` import has to be used:
138 | ```pycon
139 | >>> php.require('vendor/autoload.php')
140 |
141 | >>> import phpbridge.php.PhpParser.Node as Node # Not the namespace!
142 | >>> Node
143 |
144 | >>> from phpbridge.php.PhpParser.Node import Param # The class we want
145 | >>> Param
146 |
147 | >>> import phpbridge.php.PhpParser.Node.Param as Param # Doesn't work
148 | Traceback (most recent call last):
149 | File "", line 1, in
150 | AttributeError: type object 'PhpParser\Node' has no attribute 'Param'
151 | ```
152 |
153 | If there are no conflicts, things work as expected:
154 | ```pycon
155 | >>> from phpbridge.php.blyxxyz.PythonServer import Commands
156 | >>> Commands
157 |
158 | >>> import phpbridge.php.blyxxyz.PythonServer as PythonServer
159 | >>> PythonServer
160 |
161 | >>> PythonServer.Commands
162 |
163 | ```
164 |
165 | # Installing
166 |
167 | ```
168 | pip3 install phpbridge
169 | ```
170 |
171 | The only dependencies are PHP 7.0+, Python 3.5+, ext-json, ext-reflection and ext-mbstring. Composer can be used to install development tools and set up autoloading, but it's not required.
172 |
--------------------------------------------------------------------------------
/phpbridge/php-server/Representer/Representer.php:
--------------------------------------------------------------------------------
1 | .
17 | */
18 | class Representer implements RepresenterInterface
19 | {
20 | /**
21 | * Shorthand to get a representation without making an object manually.
22 | *
23 | * @param mixed $thing
24 | * @param int $depth
25 | * @return string
26 | */
27 | public static function r($thing, int $depth = 2): string
28 | {
29 | return (new static)->repr($thing, $depth);
30 | }
31 |
32 | const TRUE = 'true';
33 | const FALSE = 'false';
34 | const NULL = 'null';
35 |
36 | /**
37 | * Represent any value.
38 | *
39 | * $depth can be used to specify how many levels deep the representation
40 | * may go.
41 | *
42 | * @param mixed $thing
43 | * @param int $depth
44 | * @return string
45 | */
46 | public function repr($thing, int $depth = 2): string
47 | {
48 | $depth -= 1;
49 |
50 | switch (gettype($thing)) {
51 | case 'resource':
52 | return $this->reprResource($thing, $depth);
53 | case 'array':
54 | return $this->reprArray($thing, $depth);
55 | case 'boolean':
56 | return $thing ? static::TRUE : static::FALSE;
57 | case 'NULL':
58 | return static::NULL;
59 | case 'double':
60 | return $this->reprFloat($thing, $depth);
61 | case 'string':
62 | return $this->reprString($thing, $depth);
63 | case 'object':
64 | return $this->reprObject($thing, $depth);
65 | default:
66 | return $this->reprFallback($thing, $depth);
67 | }
68 | }
69 |
70 | const RESOURCE_IDEN = 'resource';
71 |
72 | /**
73 | * Represent a resource, including its type.
74 | *
75 | * @param resource $resource
76 | * @param int $depth
77 | * @return string
78 | */
79 | protected function reprResource($resource, int $depth): string
80 | {
81 | $kind = get_resource_type($resource);
82 | $id = intval($resource);
83 | return "<$kind " . static::RESOURCE_IDEN . " id #$id>";
84 | }
85 |
86 | const SEQ_ARRAY_DELIMS = ['[', ']'];
87 | const ASSOC_ARRAY_DELIMS = ['[', ']'];
88 | const KEY_SEP = ' => ';
89 | const ITEM_SEP = ', ';
90 |
91 | /**
92 | * Represent an array using modern syntax, up to a certain depth.
93 | *
94 | * @param array $array
95 | * @param int $depth
96 | * @return string
97 | */
98 | protected function reprArray(array $array, int $depth): string
99 | {
100 | if ($array === []) {
101 | return implode(static::ASSOC_ARRAY_DELIMS);
102 | }
103 | $sequential = self::arrayIsSequential($array);
104 | if ($depth <= 0) {
105 | $count = count($array);
106 | if ($sequential) {
107 | return implode(
108 | "... ($count)",
109 | static::ASSOC_ARRAY_DELIMS
110 | );
111 | } else {
112 | return implode(
113 | "..." . static::KEY_SEP . "($count)",
114 | static::SEQ_ARRAY_DELIMS
115 | );
116 | }
117 | }
118 | $content = [];
119 | if (self::arrayIsSequential($array)) {
120 | foreach ($array as $item) {
121 | $content[] = $this->repr($item, $depth);
122 | }
123 | return implode(
124 | implode(static::ITEM_SEP, $content),
125 | static::SEQ_ARRAY_DELIMS
126 | );
127 | } else {
128 | foreach ($array as $key => $item) {
129 | $content[] = $this->repr($key, $depth) . static::KEY_SEP
130 | . $this->repr($item, $depth);
131 | }
132 | return implode(
133 | implode(static::ITEM_SEP, $content),
134 | static::ASSOC_ARRAY_DELIMS
135 | );
136 | }
137 | }
138 |
139 | /**
140 | * Determine whether an array could have been created without using
141 | * associative syntax.
142 | *
143 | * @param array $array
144 | * @return bool
145 | */
146 | protected static function arrayIsSequential(array $array): bool
147 | {
148 | if (count($array) === 0) {
149 | return true;
150 | }
151 | return array_keys($array) === range(0, count($array) - 1);
152 | }
153 |
154 | const NAN = 'NAN';
155 | const INF = 'INF';
156 | const NEG_INF = '-INF';
157 |
158 | protected function reprFloat(float $thing, int $depth): string
159 | {
160 | if (is_nan($thing)) {
161 | return static::NAN;
162 | } elseif (is_infinite($thing) && $thing > 0) {
163 | return static::INF;
164 | } elseif (is_infinite($thing) && $thing < 0) {
165 | return static::NEG_INF;
166 | }
167 | return $this->reprFallback($thing, $depth);
168 | }
169 |
170 | protected function reprString(string $thing, int $depth): string
171 | {
172 | return $this->reprFallback($thing, $depth);
173 | }
174 |
175 | const OBJECT_IDEN = 'object';
176 |
177 | /**
178 | * Represent an object using its properties.
179 | *
180 | * @param object $object
181 | * @param int $depth
182 | * @return string
183 | */
184 | protected function reprObject($object, int $depth): string
185 | {
186 | if ($depth <= 0) {
187 | return $this->opaqueReprObject($object);
188 | }
189 |
190 | $cls = $this->convertClassName(get_class($object));
191 | $properties = (array)$object;
192 |
193 | if (count($properties) === 0) {
194 | return $this->opaqueReprObject($object);
195 | }
196 |
197 | $propertyReprs = [];
198 | foreach ($properties as $key => $value) {
199 | // private properties do something obscene with null bytes
200 | $keypieces = explode("\0", (string)$key);
201 | $key = $keypieces[count($keypieces) - 1];
202 | $propertyReprs[] = "$key=" . $this->repr($value, $depth);
203 | }
204 | return "<$cls " . static::OBJECT_IDEN .
205 | " (" . implode(', ', $propertyReprs) . ")>";
206 | }
207 |
208 | /**
209 | * Represent an object using only its class and hash.
210 | *
211 | * @param object $object
212 | * @return string
213 | */
214 | protected function opaqueReprObject($object): string
215 | {
216 | $cls = $this->convertClassName(get_class($object));
217 | $hash = spl_object_hash($object);
218 | if (strlen($hash) === 32) {
219 | // Get the only interesting part
220 | $hash = substr($hash, 8, 8);
221 | }
222 | return "<$cls " . static::OBJECT_IDEN . " 0x$hash>";
223 | }
224 |
225 | /**
226 | * Convert a class name to the preferred notation.
227 | *
228 | * @param string $name
229 | * @return string
230 | */
231 | protected function convertClassName(string $name): string
232 | {
233 | return $name;
234 | }
235 |
236 | /**
237 | * Represent a value if no other handler is available.
238 | *
239 | * @param mixed $thing
240 | * @param int $depth
241 | *
242 | * @return string
243 | */
244 | protected function reprFallback($thing, int $depth): string
245 | {
246 | if (is_null($thing) || is_bool($thing)) {
247 | return strtolower(var_export($thing, true));
248 | }
249 |
250 | if (is_int($thing) || is_float($thing) || is_string($thing)) {
251 | return var_export($thing, true);
252 | }
253 |
254 | // We don't know what this is so we won't risk var_export
255 | $type = gettype($thing);
256 | return "<$type>";
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/phpbridge/php-server/CommandServer.php:
--------------------------------------------------------------------------------
1 | objectStore = new ObjectStore();
23 | }
24 |
25 | /**
26 | * Receive a command from the other side of the bridge.
27 | *
28 | * Waits for a command. If one is received, returns it. If the other side
29 | * is closed, return false.
30 | *
31 | * @psalm-suppress MismatchingDocblockReturnType
32 | * @return array{cmd: string, data: mixed, garbage: array}|false
33 | */
34 | abstract public function receive(): array;
35 |
36 | /**
37 | * Send a response to the other side of the bridge.
38 | *
39 | * @param array $data
40 | *
41 | * @return void
42 | */
43 | abstract public function send(array $data);
44 |
45 | /**
46 | * Encode a value into something JSON-serializable.
47 | *
48 | * @param mixed $data
49 | *
50 | * @return array{type: string, value: mixed}
51 | */
52 | protected function encode($data): array
53 | {
54 | if (is_int($data) || is_null($data) || is_bool($data)) {
55 | return [
56 | 'type' => gettype($data),
57 | 'value' => $data
58 | ];
59 | } elseif (is_string($data)) {
60 | if (mb_check_encoding($data)) {
61 | return [
62 | 'type' => 'string',
63 | 'value' => $data
64 | ];
65 | }
66 | return [
67 | 'type' => 'bytes',
68 | 'value' => base64_encode($data)
69 | ];
70 | } elseif (is_float($data)) {
71 | if (is_nan($data) || is_infinite($data)) {
72 | $data = (string)$data;
73 | }
74 | return [
75 | 'type' => 'double',
76 | 'value' => $data
77 | ];
78 | } elseif (is_array($data)) {
79 | return [
80 | 'type' => 'array',
81 | 'value' => array_map([$this, 'encode'], $data)
82 | ];
83 | } elseif (is_object($data)) {
84 | return [
85 | 'type' => 'object',
86 | 'value' => [
87 | 'class' => get_class($data),
88 | 'hash' => $this->objectStore->encode($data)
89 | ]
90 | ];
91 | } elseif (is_resource($data)) {
92 | return [
93 | 'type' => 'resource',
94 | 'value' => [
95 | 'type' => get_resource_type($data),
96 | 'hash' => $this->objectStore->encode($data)
97 | ]
98 | ];
99 | } else {
100 | $type = gettype($data);
101 | throw new \Exception("Can't encode value of type '$type'");
102 | }
103 | }
104 |
105 | /**
106 | * Convert deserialized data into the value it represents, inverts encode.
107 | *
108 | * @param array{type: string, value: mixed} $data
109 | *
110 | * @return mixed
111 | */
112 | protected function decode(array $data)
113 | {
114 | $type = $data['type'];
115 | $value = $data['value'];
116 | switch ($type) {
117 | case 'integer':
118 | case 'string':
119 | case 'NULL':
120 | case 'boolean':
121 | return $value;
122 | case 'double':
123 | if ($value === 'NAN') {
124 | return NAN;
125 | } elseif ($value === 'INF') {
126 | return INF;
127 | } elseif ($value === '-INF') {
128 | return -INF;
129 | }
130 | return $value;
131 | case 'array':
132 | return $this->decodeArray($value);
133 | case 'object':
134 | case 'resource':
135 | return $this->objectStore->decode($value['hash']);
136 | case 'bytes':
137 | return base64_decode($value);
138 | default:
139 | throw new \Exception("Unknown type '$type'");
140 | }
141 | }
142 |
143 | /**
144 | * Decode an array of values.
145 | *
146 | * @param array $dataItems
147 | *
148 | * @return array
149 | */
150 | protected function decodeArray(array $dataItems)
151 | {
152 | return array_map([$this, 'decode'], $dataItems);
153 | }
154 |
155 | /**
156 | * Continually listen for commands.
157 | *
158 | * @return void
159 | */
160 | public function communicate()
161 | {
162 | while (($command = $this->receive()) !== false) {
163 | $cmd = $command['cmd'];
164 | $data = $command['data'];
165 | $garbage = $command['garbage'];
166 | $collected = [];
167 | try {
168 | foreach ($garbage as $key) {
169 | // It might have been removed before, but ObjectStore
170 | // doesn't mind
171 | $this->objectStore->remove($key);
172 | $collected[] = $key;
173 | }
174 | $response = $this->execute($cmd, $data);
175 | } catch (\Throwable $exception) {
176 | $this->send($this->encodeThrownException(
177 | $exception,
178 | $collected
179 | ));
180 | continue;
181 | }
182 | $this->send([
183 | 'type' => 'result',
184 | 'data' => $response,
185 | 'collected' => $collected
186 | ]);
187 | }
188 | }
189 |
190 | /**
191 | * Encode an exception into a thrownException response.
192 | *
193 | * @param \Throwable $exception
194 | * @param array $collected
195 | * @return array
196 | */
197 | protected function encodeThrownException(
198 | \Throwable $exception,
199 | array $collected = []
200 | ): array {
201 | return [
202 | 'type' => 'exception',
203 | 'data' => [
204 | 'value' => $this->encode($exception),
205 | 'message' => $exception->getMessage()
206 | ],
207 | 'collected' => $collected
208 | ];
209 | }
210 |
211 | /**
212 | * Execute a command and return the (unencoded) result.
213 | *
214 | * @param string $command The name of the command
215 | * @param mixed $data The parameters of the commands
216 | *
217 | * @return mixed
218 | */
219 | private function execute(string $command, $data)
220 | {
221 | switch ($command) {
222 | case 'getConst':
223 | return $this->encode(Commands::getConst($data));
224 | case 'setConst':
225 | return Commands::setConst(
226 | $data['name'],
227 | $this->decode($data['value'])
228 | );
229 | case 'getGlobal':
230 | return $this->encode(Commands::getGlobal($data));
231 | case 'setGlobal':
232 | return Commands::setGlobal(
233 | $data['name'],
234 | $this->decode($data['value'])
235 | );
236 | case 'callFun':
237 | return $this->encode(Commands::callFun(
238 | $data['name'],
239 | $this->decodeArray($data['args'])
240 | ));
241 | case 'callObj':
242 | return $this->encode(Commands::callObj(
243 | $this->decode($data['obj']),
244 | $this->decodeArray($data['args'])
245 | ));
246 | case 'callMethod':
247 | return $this->encode(Commands::callMethod(
248 | $this->decode($data['obj']),
249 | $data['name'],
250 | $this->decodeArray($data['args'])
251 | ));
252 | case 'hasItem':
253 | return Commands::hasItem(
254 | $this->decode($data['obj']),
255 | $this->decode($data['offset'])
256 | );
257 | case 'getItem':
258 | return $this->encode(Commands::getItem(
259 | $this->decode($data['obj']),
260 | $this->decode($data['offset'])
261 | ));
262 | case 'setItem':
263 | return Commands::setItem(
264 | $this->decode($data['obj']),
265 | $this->decode($data['offset']),
266 | $this->decode($data['value'])
267 | );
268 | case 'delItem':
269 | return Commands::delItem(
270 | $this->decode($data['obj']),
271 | $this->decode($data['offset'])
272 | );
273 | case 'createObject':
274 | return $this->encode(Commands::createObject(
275 | $data['name'],
276 | $this->decodeArray($data['args'])
277 | ));
278 | case 'getProperty':
279 | return $this->encode(Commands::getProperty(
280 | $this->decode($data['obj']),
281 | $data['name']
282 | ));
283 | case 'setProperty':
284 | return Commands::setProperty(
285 | $this->decode($data['obj']),
286 | $data['name'],
287 | $this->decode($data['value'])
288 | );
289 | case 'unsetProperty':
290 | return Commands::unsetProperty(
291 | $this->decode($data['obj']),
292 | $data['name']
293 | );
294 | case 'listNonDefaultProperties':
295 | return Commands::listNonDefaultProperties($this->decode($data));
296 | case 'classInfo':
297 | $classInfo = Commands::classInfo($data);
298 | foreach ($classInfo['methods'] as &$method) {
299 | foreach ($method['params'] as &$param) {
300 | $param['default'] = $this->encode($param['default']);
301 | }
302 | }
303 | return $classInfo;
304 | case 'funcInfo':
305 | $funcInfo = Commands::funcInfo($data);
306 | foreach ($funcInfo['params'] as &$param) {
307 | $param['default'] = $this->encode($param['default']);
308 | }
309 | return $funcInfo;
310 | case 'listConsts':
311 | return Commands::listConsts();
312 | case 'listGlobals':
313 | return Commands::listGlobals();
314 | case 'listFuns':
315 | return Commands::listFuns();
316 | case 'listClasses':
317 | return Commands::listClasses();
318 | case 'listEverything':
319 | return iterator_to_array(Commands::listEverything($data));
320 | case 'listNamespaces':
321 | return Commands::listNamespaces($data);
322 | case 'resolveName':
323 | return Commands::resolveName($data);
324 | case 'repr':
325 | return $this->encode(Commands::repr($this->decode($data)));
326 | case 'str':
327 | return $this->encode(Commands::str($this->decode($data)));
328 | case 'count':
329 | return Commands::count($this->decode($data));
330 | case 'startIteration':
331 | return $this->encode(Commands::startIteration(
332 | $this->decode($data)
333 | ));
334 | case 'nextIteration':
335 | return $this->encode(Commands::nextIteration(
336 | $this->decode($data)
337 | ));
338 | case 'throwException':
339 | Commands::throwException(
340 | $data['class'],
341 | $data['message']
342 | );
343 | return null;
344 | default:
345 | throw new \Exception("Unknown command '$command'");
346 | }
347 | }
348 | }
349 |
--------------------------------------------------------------------------------
/phpbridge/objects.py:
--------------------------------------------------------------------------------
1 | """Translation of PHP classes and objects to Python."""
2 |
3 | from itertools import product
4 | from typing import (Any, Callable, Dict, List, Optional, Type, # noqa: F401
5 | Union)
6 | from warnings import warn
7 |
8 | from phpbridge.functions import make_signature, default_constructor_signature
9 | from phpbridge import modules, utils
10 |
11 | MYPY = False
12 | if MYPY:
13 | from phpbridge import PHPBridge # noqa: F401
14 |
15 |
16 | class PHPClass(type):
17 | """The metaclass of all PHP classes and interfaces."""
18 | _bridge = None # type: PHPBridge
19 | _name = None # type: str
20 | _is_abstract = False
21 | _is_interface = False
22 | _is_trait = False
23 |
24 | def __call__(self, *a: Any, **kw: Any) -> Any:
25 | if self._is_trait:
26 | raise TypeError("Cannot instantiate trait {}".format(
27 | self.__name__))
28 | elif self._is_interface:
29 | raise TypeError("Cannot instantiate interface {}".format(
30 | self.__name__))
31 | elif self._is_abstract:
32 | raise TypeError("Cannot instantiate abstract class {}".format(
33 | self.__name__))
34 | return super().__call__(*a, **kw)
35 |
36 | def __repr__(self) -> str:
37 | if self._is_trait:
38 | return "".format(self.__name__)
39 | elif self._is_interface:
40 | return "".format(self.__name__)
41 | elif self._is_abstract:
42 | return "".format(self.__name__)
43 | return "".format(self.__name__)
44 |
45 |
46 | class PHPObject(metaclass=PHPClass):
47 | """The base class of all instantiatable PHP classes."""
48 | def __new__(cls, *args: Any) -> Any:
49 | """Create and return a new object."""
50 | return cls._bridge.send_command(
51 | 'createObject',
52 | {'name': cls._name,
53 | 'args': [cls._bridge.encode(arg) for arg in args]},
54 | decode=True)
55 |
56 | # In theory, this __new__ only shows up if no constructor has been defined,
57 | # so it doesn't take arguments. In practice we don't want to enforce that,
58 | # but we'll override the ugly signature.
59 | __new__.__signature__ = default_constructor_signature # type: ignore
60 |
61 | def __repr__(self) -> str:
62 | return self._bridge.send_command( # type: ignore
63 | 'repr', self._bridge.encode(self), decode=True)
64 |
65 | def __getattr__(self, attr: str) -> Any:
66 | return self._bridge.send_command(
67 | 'getProperty',
68 | {'obj': self._bridge.encode(self),
69 | 'name': attr},
70 | decode=True)
71 |
72 | def __setattr__(self, attr: str, value: Any) -> None:
73 | self._bridge.send_command(
74 | 'setProperty',
75 | {'obj': self._bridge.encode(self),
76 | 'name': attr,
77 | 'value': self._bridge.encode(value)})
78 |
79 | def __delattr__(self, attr: str) -> None:
80 | self._bridge.send_command(
81 | 'unsetProperty',
82 | {'obj': self._bridge.encode(self),
83 | 'name': attr})
84 |
85 | def __dir__(self) -> List[str]:
86 | return super().__dir__() + self._bridge.send_command( # type: ignore
87 | 'listNonDefaultProperties', self._bridge.encode(self))
88 |
89 |
90 | def make_method(bridge: 'PHPBridge', classname: str, name: str,
91 | info: dict) -> Callable:
92 |
93 | def method(*args: Any, **kwargs: Any) -> Any:
94 | self, *args = utils.parse_args(
95 | method.__signature__, args, kwargs) # type: ignore
96 | return bridge.send_command(
97 | 'callMethod',
98 | {'obj': bridge.encode(self),
99 | 'name': name,
100 | 'args': [bridge.encode(arg) for arg in args]},
101 | decode=True)
102 |
103 | method.__module__ = modules.get_module(bridge, classname)
104 | method.__name__ = name
105 | method.__qualname__ = modules.basename(classname) + '.' + name
106 |
107 | if info['doc'] is not False:
108 | method.__doc__ = utils.convert_docblock(info['doc'])
109 |
110 | if info['static']:
111 | # mypy doesn't know classmethods are callable
112 | return classmethod(method) # type: ignore
113 |
114 | return method
115 |
116 |
117 | def create_property(name: str, doc: Optional[str]) -> property:
118 | def getter(self: PHPObject) -> Any:
119 | return self._bridge.send_command(
120 | 'getProperty',
121 | {'obj': self._bridge.encode(self), 'name': name},
122 | decode=True)
123 |
124 | def setter(self: PHPObject, value: Any) -> None:
125 | self._bridge.send_command(
126 | 'setProperty',
127 | {'obj': self._bridge.encode(self),
128 | 'name': name,
129 | 'value': self._bridge.encode(value)})
130 |
131 | def deleter(self: PHPObject) -> None:
132 | self._bridge.send_command(
133 | 'unsetProperty', {'obj': self._bridge.encode(self), 'name': name})
134 |
135 | getter.__doc__ = doc
136 |
137 | return property(getter, setter, deleter)
138 |
139 |
140 | def create_class(bridge: 'PHPBridge', unresolved_classname: str) -> None:
141 | """Create and register a PHPClass.
142 |
143 | Args:
144 | bridge: The bridge the class belongs to.
145 | unresolved_classname: The name of the class.
146 | """
147 | info = bridge.send_command('classInfo', unresolved_classname)
148 |
149 | classname = info['name'] # type: str
150 | methods = info['methods'] # type: Dict[str, Dict[str, Any]]
151 | interfaces = info['interfaces'] # type: List[str]
152 | traits = info['traits'] # type: List[str]
153 | consts = info['consts'] # type: Dict[str, Any]
154 | properties = info['properties'] # type: Dict[str, Dict[str, Any]]
155 | doc = info['doc'] # type: Optional[str]
156 | parent = info['parent'] # type: str
157 | is_abstract = info['isAbstract'] # type: bool
158 | is_interface = info['isInterface'] # type: bool
159 | is_trait = info['isTrait'] # type: bool
160 |
161 | # PHP turns empty associative arrays into empty lists, so
162 | if not properties:
163 | properties = {}
164 | if not consts:
165 | consts = {}
166 | if not methods:
167 | methods = {}
168 |
169 | # "\Foo" resolves to the same class as "Foo", so we want them to be the
170 | # same Python objects as well
171 | if classname in bridge.classes:
172 | bridge.classes[unresolved_classname] = bridge.classes[classname]
173 | return
174 |
175 | bases = [PHPObject] # type: List[Type]
176 |
177 | if parent is not False:
178 | bases.append(bridge.get_class(parent))
179 |
180 | for trait in traits:
181 | bases.append(bridge.get_class(trait))
182 |
183 | for interface in interfaces:
184 | bases.append(bridge.get_class(interface))
185 |
186 | bindings = {} # type: Dict[str, Any]
187 |
188 | for name, value in consts.items():
189 | bindings[name] = value
190 |
191 | for name, property_info in properties.items():
192 | if name in bindings:
193 | warn("'{}' on class '{}' has multiple meanings".format(
194 | name, classname))
195 | property_doc = utils.convert_docblock(property_info['doc'])
196 | bindings[name] = create_property(name, property_doc)
197 |
198 | created_methods = {} # type: Dict[str, Callable]
199 |
200 | from phpbridge.classes import magic_aliases
201 | for name, method_info in methods.items():
202 | if name in bindings:
203 | warn("'{}' on class '{}' has multiple meanings".format(
204 | name, classname))
205 | if method_info['owner'] != classname:
206 | # Make inheritance visible
207 | continue
208 |
209 | method = make_method(bridge, classname, name, method_info)
210 |
211 | if (isinstance(method.__doc__, str) and
212 | '@inheritdoc' in method.__doc__.lower()):
213 | # If @inheritdoc is used, we manually look for inheritance.
214 | # If method.__doc__ is empty we leave it empty, and pydoc and
215 | # inspect know where to look.
216 | for base in bases:
217 | try:
218 | base_doc = getattr(base, name).__doc__
219 | if isinstance(base_doc, str):
220 | method.__doc__ = base_doc
221 | break
222 | except AttributeError:
223 | pass
224 |
225 | bindings[name] = method
226 | created_methods[name] = method
227 | if name in magic_aliases:
228 | bindings[magic_aliases[name]] = method
229 |
230 | if method_info['isConstructor']:
231 |
232 | def __new__(*args: Any, **kwargs: Any) -> Any:
233 | cls, *args = utils.parse_args(
234 | __new__.__signature__, args, kwargs) # type: ignore
235 | return PHPObject.__new__(cls, *args)
236 |
237 | __new__.__module__ = method.__module__
238 | __new__.__qualname__ = modules.basename(classname) + '.__new__'
239 | __new__.__doc__ = method.__doc__
240 | bindings['__new__'] = __new__
241 |
242 | # Bind the magic methods needed to make these interfaces work
243 | # TODO: figure out something less ugly
244 | from phpbridge.classes import predef_classes
245 | if classname in predef_classes:
246 | predef = predef_classes[classname]
247 | for name, value in predef.__dict__.items():
248 | if callable(value):
249 | bindings[name] = value
250 | if doc is False:
251 | doc = predef.__doc__
252 | bases += predef.__bases__
253 |
254 | # Remove redundant bases
255 | while True:
256 | for (ind_a, a), (ind_b, b) in product(enumerate(bases),
257 | enumerate(bases)):
258 | if ind_a != ind_b and issubclass(a, b):
259 | # Don't use list.remove because maybe a == b
260 | # It's cleaner to keep the first occurrence
261 | del bases[ind_b]
262 | # Restart the loop because we modified bases
263 | break
264 | else:
265 | break
266 |
267 | # do this last to make sure it isn't replaced
268 | bindings['_bridge'] = bridge
269 | bindings['__doc__'] = utils.convert_docblock(doc)
270 | bindings['__module__'] = modules.get_module(bridge, classname)
271 | bindings['_is_abstract'] = is_abstract
272 | bindings['_is_interface'] = is_interface
273 | bindings['_is_trait'] = is_trait
274 |
275 | # PHP does something really nasty when you make an anonymous class.
276 | # Each class needs to have a unique name, so it makes a name that goes
277 | # class@anonymous
278 | # The null byte is probably to trick naively written C code into
279 | # printing only the class@anonymous part.
280 | # Unfortunately, Python doesn't like those class names, so we'll
281 | # insert another character that you'll (hopefully) not find in any
282 | # named class's name because the syntax doesn't allow it.
283 | typename = classname.replace('\0', '$')
284 | bindings['_name'] = classname
285 |
286 | cls = PHPClass(typename, tuple(bases), bindings)
287 |
288 | cls.__qualname__ = modules.basename(cls.__name__)
289 |
290 | bridge.classes[unresolved_classname] = cls
291 | bridge.classes[classname] = cls
292 |
293 | # Only now do we attach signatures, because the signatures may contain the
294 | # class we just registered
295 | for name, func in created_methods.items():
296 | method_info = methods[name]
297 | if method_info['static']:
298 | # classmethod
299 | func = func.__func__ # type: ignore
300 | signature = make_signature(bridge, method_info, add_first='self')
301 | func.__signature__ = signature # type: ignore
302 |
303 | if method_info['isConstructor']:
304 | cls.__new__.__signature__ = make_signature( # type: ignore
305 | bridge, method_info, add_first='cls')
306 |
307 |
308 | class PHPResource:
309 | """A representation of a remote resource value.
310 |
311 | Not technically an object, but similar in a lot of ways. Resources have a
312 | type (represented by a string) and an identifier used for reference
313 | counting.
314 | """
315 | def __init__(self, bridge: 'PHPBridge', type_: str, id_: int) -> None:
316 | # Leading underscores are not necessary here but nice for consistency
317 | self._bridge = bridge
318 | self._type = type_
319 | self._id = id_
320 |
321 | def __repr__(self) -> str:
322 | """Mimics print_r output for resources, but more informative."""
323 | return "".format(self._type, self._id)
324 |
325 |
326 | php_types = {
327 | 'int': int,
328 | 'integer': int,
329 | 'bool': bool,
330 | 'boolean': bool,
331 | 'array': dict,
332 | 'float': float,
333 | 'double': float,
334 | 'string': str,
335 | 'void': None,
336 | 'NULL': None,
337 | 'null': None,
338 | 'callable': Callable,
339 | 'true': True,
340 | 'false': False,
341 | 'mixed': Any,
342 | 'object': PHPObject,
343 | 'resource': PHPResource
344 | }
345 |
--------------------------------------------------------------------------------
/phpbridge/__init__.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import json
3 | import math
4 | import os
5 | import subprocess as sp
6 | import sys
7 | import types
8 |
9 | from collections import ChainMap, OrderedDict
10 | from typing import (Any, Callable, IO, Iterator, List, Dict, # noqa: F401
11 | Iterable, Optional, Set, Union)
12 | from weakref import finalize
13 |
14 | from phpbridge import functions, modules, objects
15 |
16 | php_server_path = os.path.join(
17 | os.path.dirname(__file__), 'server.php')
18 |
19 |
20 | class PHPBridge:
21 | def __init__(self, input_: IO[str], output: IO[str], name: str) -> None:
22 | self.input = input_
23 | self.output = output
24 | self.classes = {} # type: Dict[str, objects.PHPClass]
25 | self.functions = {} # type: Dict[str, Callable]
26 | self.constants = {} # type: Dict[str, Any]
27 | self.cache = ChainMap(self.classes, self.functions, self.constants)
28 | self._remotes = {} # type: Dict[Union[int, str], finalize]
29 | self._collected = set() # type: Set[Union[int, str]]
30 | self._debug = False
31 | self.__name__ = name
32 |
33 | def send(self, command: str, data: Any) -> None:
34 | if self._debug:
35 | print(command, data)
36 | garbage = list(self._collected.copy())
37 | if self._debug and garbage:
38 | print("Asking to collect {}".format(garbage))
39 | json.dump({'cmd': command,
40 | 'data': data,
41 | 'garbage': garbage},
42 | self.input)
43 | self.input.write('\n')
44 | self.input.flush()
45 |
46 | def receive(self) -> Any:
47 | line = self.output.readline()
48 | if self._debug:
49 | print(line, end='')
50 | if not line:
51 | # Empty response, not even a newline
52 | raise RuntimeError("Connection closed")
53 | response = json.loads(line)
54 | for key in response['collected']:
55 | if self._debug:
56 | print("Confirmed {} collected".format(key))
57 | if key in self._collected:
58 | self._collected.remove(key)
59 | else:
60 | if self._debug:
61 | print("But {} is not pending collection".format(key))
62 | if response['type'] == 'exception':
63 | try:
64 | exception = self.decode(response['data']['value'])
65 | except Exception:
66 | raise Exception(
67 | "Failed decoding exception with message '{}'".format(
68 | response['data']['message']))
69 | raise exception
70 | elif response['type'] == 'result':
71 | return response['data']
72 | else:
73 | raise Exception("Received response with unknown type {}".format(
74 | response['type']))
75 |
76 | def encode(self, data: Any) -> dict:
77 | if isinstance(data, str):
78 | try:
79 | data.encode()
80 | return {'type': 'string', 'value': data}
81 | except UnicodeEncodeError:
82 | # string contains surrogates
83 | data = data.encode(errors='surrogateescape')
84 |
85 | if isinstance(data, bytes):
86 | return {'type': 'bytes',
87 | 'value': base64.b64encode(data).decode()}
88 |
89 | if isinstance(data, bool):
90 | return {'type': 'boolean', 'value': data}
91 |
92 | if isinstance(data, int):
93 | return {'type': 'integer', 'value': data}
94 |
95 | if isinstance(data, float):
96 | if math.isnan(data):
97 | data = 'NAN'
98 | elif math.isinf(data):
99 | data = 'INF' if data > 0 else '-INF'
100 | return {'type': 'double', 'value': data}
101 |
102 | if data is None:
103 | return {'type': 'NULL', 'value': data}
104 |
105 | if isinstance(data, dict) and all(
106 | isinstance(key, str) or isinstance(key, int)
107 | for key in data):
108 | return {'type': 'array', 'value': {k: self.encode(v)
109 | for k, v in data.items()}}
110 | if isinstance(data, list):
111 | return {'type': 'array', 'value': [self.encode(item)
112 | for item in data]}
113 |
114 | if isinstance(data, objects.PHPObject) and data._bridge is self:
115 | return {'type': 'object',
116 | 'value': {'class': data.__class__._name, # type: ignore
117 | 'hash': data._hash}}
118 |
119 | if isinstance(data, objects.PHPResource) and data._bridge is self:
120 | return {'type': 'resource',
121 | 'value': {'type': data._type,
122 | 'hash': data._id}}
123 |
124 | if isinstance(data, objects.PHPClass) and data._bridge is self:
125 | # PHP uses strings to represent functions and classes
126 | # This unfortunately means they will be strings if they come back
127 | return {'type': 'string', 'value': data._name}
128 |
129 | if (isinstance(data, types.FunctionType) and
130 | getattr(data, '_bridge', None) is self):
131 | return {'type': 'string', 'value': data.__name__}
132 |
133 | if (isinstance(data, types.MethodType) and
134 | getattr(data.__self__, '_bridge', None) is self):
135 | return self.encode([data.__self__, data.__name__])
136 |
137 | raise RuntimeError("Can't encode {!r}".format(data))
138 |
139 | def decode(self, data: dict) -> Any:
140 | type_ = data['type']
141 | value = data['value']
142 | if type_ in {'string', 'integer', 'NULL', 'boolean'}:
143 | return value
144 | elif type_ == 'double':
145 | if value == 'INF':
146 | return math.inf
147 | elif value == '-INF':
148 | return -math.inf
149 | elif value == 'NAN':
150 | return math.nan
151 | return value
152 | elif type_ == 'array':
153 | if isinstance(value, list):
154 | return Array.list(map(self.decode, value))
155 | elif isinstance(value, dict):
156 | return Array((key, self.decode(item))
157 | for key, item in value.items())
158 | elif type_ == 'object':
159 | cls = self.get_class(value['class'])
160 | return self.get_object(cls, value['hash'])
161 | elif type_ == 'resource':
162 | return self.get_resource(value['type'], value['hash'])
163 | elif type_ == 'bytes':
164 | # PHP's strings are just byte arrays
165 | # Decoding this to a bytes object would be problematic
166 | # It might be meant as a legitimate string, and some binary data
167 | # could be valid unicode by accident
168 | value = base64.b64decode(value)
169 | return value.decode(errors='surrogateescape')
170 | raise RuntimeError("Unknown type {!r}".format(type_))
171 |
172 | def send_command(self, cmd: str, data: Any = None,
173 | decode: bool = False) -> Any:
174 | self.send(cmd, data)
175 | result = self.receive()
176 | if decode:
177 | result = self.decode(result)
178 | return result
179 |
180 | def resolve(self, path: str, name: str) -> Any:
181 | if path:
182 | name = path + '\\' + name
183 |
184 | if name in self.cache:
185 | return self.cache[name]
186 | else:
187 | kind = self.send_command('resolveName', name)
188 |
189 | if kind == 'class':
190 | return self.get_class(name)
191 | elif kind == 'func':
192 | return self.get_function(name)
193 | elif kind == 'const':
194 | return self.get_const(name)
195 | elif kind == 'global':
196 | return self.get_global(name)
197 | elif kind == 'none':
198 | raise AttributeError("Nothing named '{}' found".format(name))
199 | else:
200 | raise RuntimeError("Resolved unknown data type {}".format(kind))
201 |
202 | def get_class(self, name: str) -> objects.PHPClass:
203 | if name not in self.classes:
204 | objects.create_class(self, name)
205 | return self.classes[name]
206 |
207 | def get_function(self, name: str) -> Callable:
208 | if name not in self.functions:
209 | functions.create_function(self, name)
210 | return self.functions[name]
211 |
212 | def get_const(self, name: str) -> Any:
213 | if name not in self.constants:
214 | self.constants[name] = self.send_command(
215 | 'getConst', name, decode=True)
216 | return self.constants[name]
217 |
218 | def get_global(self, name: str) -> Any:
219 | return self.send_command('getGlobal', name, decode=True)
220 |
221 | def get_object(self, cls: objects.PHPClass,
222 | hash_: str) -> objects.PHPObject:
223 | obj = self._lookup(hash_)
224 | if obj is not None:
225 | return obj # type: ignore
226 | new_obj = super(objects.PHPObject, cls).__new__(cls) # type: ignore
227 | object.__setattr__(new_obj, '_hash', hash_)
228 | self._register(hash_, new_obj)
229 | return new_obj # type: ignore
230 |
231 | def get_resource(self, type_: str, id_: int) -> objects.PHPResource:
232 | resource = self._lookup(id_)
233 | if resource is not None:
234 | return resource # type: ignore
235 | new_resource = objects.PHPResource(self, type_, id_)
236 | self._register(id_, new_resource)
237 | return new_resource
238 |
239 | def _register(self, ident: Union[int, str],
240 | entity: Union[objects.PHPResource,
241 | objects.PHPObject]) -> None:
242 | """Register an object or resource with a weakref."""
243 | if self._debug:
244 | print("Registering {}".format(ident))
245 | self._remotes[ident] = finalize(entity, self._collect, ident)
246 |
247 | def _lookup(self, ident: Union[int, str]) -> Optional[
248 | Union[objects.PHPResource, objects.PHPObject]]:
249 | """Look up an existing object for a remote entity."""
250 | try:
251 | ref = self._remotes[ident]
252 | except KeyError:
253 | return None
254 | contents = ref.peek()
255 | if contents is None:
256 | return None
257 | return contents[0]
258 |
259 | def _collect(self, ident: Union[int, str]) -> None:
260 | """Mark an object or resource identifier as garbage collected."""
261 | if self._debug:
262 | print("Lost {}".format(ident))
263 | self._collected.add(ident)
264 | del self._remotes[ident]
265 |
266 |
267 | class Array(OrderedDict):
268 | """An ordered dictionary with some of PHP's idiosyncrasies.
269 |
270 | These can be treated like lists, to some extent. Simple looping yields
271 | values, not keys. To get keys, explicitly use .keys(). Positive integer
272 | keys are automatically converted to strings.
273 |
274 | Creating these arrays yourself or modifying them is a bad idea. This class
275 | only exists to deal with PHP's ambiguities. If not consumed immediately,
276 | it's best to convert it to a list or a dict, depending on the kind of array
277 | you expect.
278 | """
279 | def __iter__(self) -> Iterator:
280 | yield from self.values()
281 |
282 | def __getitem__(self, index: Union[int, str, slice]) -> Any:
283 | if isinstance(index, slice) or isinstance(index, int) and index < 0:
284 | return list(self.values())[index]
285 | if isinstance(index, int):
286 | index = str(index)
287 | return super().__getitem__(index)
288 |
289 | def __contains__(self, value: Any) -> bool:
290 | return value in self.values()
291 |
292 | def __setitem__(self, index: Union[int, str], value: Any) -> None:
293 | if isinstance(index, int):
294 | index = str(index)
295 | super().__setitem__(index, value)
296 |
297 | def __delitem__(self, index: Union[int, str]) -> None:
298 | if isinstance(index, int):
299 | index = str(index)
300 | super().__delitem__(index)
301 |
302 | def listable(self) -> bool:
303 | """Return whether the array could be created from a list."""
304 | return all(str(ind) == key for ind, key in enumerate(self.keys()))
305 |
306 | @classmethod
307 | def list(cls, iterable: Iterable) -> 'Array':
308 | """Create by taking values from a list and using indexes as keys."""
309 | return cls((str(ind), item) # type: ignore
310 | for ind, item in enumerate(iterable))
311 |
312 | def __repr__(self) -> str:
313 | if self and self.listable():
314 | return "{}.list({})".format(self.__class__.__name__,
315 | list(self.values()))
316 | return super().__repr__()
317 |
318 |
319 | def start_process_unix(fname: str, name: str) -> PHPBridge:
320 | """Start a server.php bridge using two pipes.
321 |
322 | pass_fds is not supported on Windows. It may be that some other way to
323 | inherit file descriptors exists on Windows. In that case, the Windows
324 | function should be adjusted, or merged with this one.
325 | """
326 | php_in, py_in = os.pipe()
327 | py_out, php_out = os.pipe()
328 | sp.Popen(['php', fname, 'php://fd/{}'.format(php_in),
329 | 'php://fd/{}'.format(php_out)],
330 | pass_fds=[0, 1, 2, php_in, php_out])
331 | os.close(php_in)
332 | os.close(php_out)
333 | return PHPBridge(os.fdopen(py_in, 'w'), os.fdopen(py_out, 'r'), name)
334 |
335 |
336 | def start_process_windows(fname: str, name: str) -> PHPBridge:
337 | """Start a server.php bridge over stdin and stderr."""
338 | proc = sp.Popen(['php', fname, 'php://stdin', 'php://stderr'],
339 | stdin=sp.PIPE, stderr=sp.PIPE,
340 | universal_newlines=True)
341 | return PHPBridge(proc.stdin, proc.stderr, name)
342 |
343 |
344 | def start_process(fname: str = php_server_path,
345 | name: str = 'php') -> PHPBridge:
346 | """Start server.php and open a bridge to it."""
347 | if sys.platform.startswith('win32'):
348 | return start_process_windows(fname, name)
349 | return start_process_unix(fname, name)
350 |
351 |
352 | modules.NamespaceFinder(start_process, 'php').register()
353 |
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "7e24cafb0cff3c1d3b45d96a56b57b15",
8 | "packages": [],
9 | "packages-dev": [
10 | {
11 | "name": "composer/xdebug-handler",
12 | "version": "1.3.1",
13 | "source": {
14 | "type": "git",
15 | "url": "https://github.com/composer/xdebug-handler.git",
16 | "reference": "dc523135366eb68f22268d069ea7749486458562"
17 | },
18 | "dist": {
19 | "type": "zip",
20 | "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/dc523135366eb68f22268d069ea7749486458562",
21 | "reference": "dc523135366eb68f22268d069ea7749486458562",
22 | "shasum": ""
23 | },
24 | "require": {
25 | "php": "^5.3.2 || ^7.0",
26 | "psr/log": "^1.0"
27 | },
28 | "require-dev": {
29 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
30 | },
31 | "type": "library",
32 | "autoload": {
33 | "psr-4": {
34 | "Composer\\XdebugHandler\\": "src"
35 | }
36 | },
37 | "notification-url": "https://packagist.org/downloads/",
38 | "license": [
39 | "MIT"
40 | ],
41 | "authors": [
42 | {
43 | "name": "John Stevenson",
44 | "email": "john-stevenson@blueyonder.co.uk"
45 | }
46 | ],
47 | "description": "Restarts a process without xdebug.",
48 | "keywords": [
49 | "Xdebug",
50 | "performance"
51 | ],
52 | "time": "2018-11-29T10:59:02+00:00"
53 | },
54 | {
55 | "name": "muglug/package-versions-56",
56 | "version": "1.2.4",
57 | "source": {
58 | "type": "git",
59 | "url": "https://github.com/muglug/PackageVersions.git",
60 | "reference": "a67bed26deaaf9269a348e53063bc8d4dcc60ffd"
61 | },
62 | "dist": {
63 | "type": "zip",
64 | "url": "https://api.github.com/repos/muglug/PackageVersions/zipball/a67bed26deaaf9269a348e53063bc8d4dcc60ffd",
65 | "reference": "a67bed26deaaf9269a348e53063bc8d4dcc60ffd",
66 | "shasum": ""
67 | },
68 | "require": {
69 | "composer-plugin-api": "^1.0",
70 | "php": "^5.6 || ^7.0"
71 | },
72 | "require-dev": {
73 | "composer/composer": "^1.3",
74 | "ext-zip": "*",
75 | "phpunit/phpunit": "^5.7.5"
76 | },
77 | "type": "composer-plugin",
78 | "extra": {
79 | "class": "Muglug\\PackageVersions\\Installer",
80 | "branch-alias": {
81 | "dev-master": "2.0.x-dev"
82 | }
83 | },
84 | "autoload": {
85 | "psr-4": {
86 | "Muglug\\PackageVersions\\": "src/PackageVersions"
87 | }
88 | },
89 | "notification-url": "https://packagist.org/downloads/",
90 | "license": [
91 | "MIT"
92 | ],
93 | "authors": [
94 | {
95 | "name": "Marco Pivetta",
96 | "email": "ocramius@gmail.com"
97 | },
98 | {
99 | "name": "Abdul Malik Ikhsan",
100 | "email": "samsonasik@gmail.com"
101 | },
102 | {
103 | "name": "Matt Brown",
104 | "email": "github@muglug.com"
105 | }
106 | ],
107 | "description": "A backport of ocramius/package-versions that supports php ^5.6. Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
108 | "time": "2018-03-26T03:22:13+00:00"
109 | },
110 | {
111 | "name": "nikic/php-parser",
112 | "version": "v3.1.5",
113 | "source": {
114 | "type": "git",
115 | "url": "https://github.com/nikic/PHP-Parser.git",
116 | "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce"
117 | },
118 | "dist": {
119 | "type": "zip",
120 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/bb87e28e7d7b8d9a7fda231d37457c9210faf6ce",
121 | "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce",
122 | "shasum": ""
123 | },
124 | "require": {
125 | "ext-tokenizer": "*",
126 | "php": ">=5.5"
127 | },
128 | "require-dev": {
129 | "phpunit/phpunit": "~4.0|~5.0"
130 | },
131 | "bin": [
132 | "bin/php-parse"
133 | ],
134 | "type": "library",
135 | "extra": {
136 | "branch-alias": {
137 | "dev-master": "3.0-dev"
138 | }
139 | },
140 | "autoload": {
141 | "psr-4": {
142 | "PhpParser\\": "lib/PhpParser"
143 | }
144 | },
145 | "notification-url": "https://packagist.org/downloads/",
146 | "license": [
147 | "BSD-3-Clause"
148 | ],
149 | "authors": [
150 | {
151 | "name": "Nikita Popov"
152 | }
153 | ],
154 | "description": "A PHP parser written in PHP",
155 | "keywords": [
156 | "parser",
157 | "php"
158 | ],
159 | "time": "2018-02-28T20:30:58+00:00"
160 | },
161 | {
162 | "name": "openlss/lib-array2xml",
163 | "version": "0.5.1",
164 | "source": {
165 | "type": "git",
166 | "url": "https://github.com/nullivex/lib-array2xml.git",
167 | "reference": "c8b5998a342d7861f2e921403f44e0a2f3ef2be0"
168 | },
169 | "dist": {
170 | "type": "zip",
171 | "url": "https://api.github.com/repos/nullivex/lib-array2xml/zipball/c8b5998a342d7861f2e921403f44e0a2f3ef2be0",
172 | "reference": "c8b5998a342d7861f2e921403f44e0a2f3ef2be0",
173 | "shasum": ""
174 | },
175 | "require": {
176 | "php": ">=5.3.2"
177 | },
178 | "type": "library",
179 | "autoload": {
180 | "psr-0": {
181 | "LSS": ""
182 | }
183 | },
184 | "notification-url": "https://packagist.org/downloads/",
185 | "license": [
186 | "Apache-2.0"
187 | ],
188 | "authors": [
189 | {
190 | "name": "Bryan Tong",
191 | "email": "contact@nullivex.com",
192 | "homepage": "http://bryantong.com"
193 | },
194 | {
195 | "name": "Tony Butler",
196 | "email": "spudz76@gmail.com",
197 | "homepage": "http://openlss.org"
198 | }
199 | ],
200 | "description": "Array2XML conversion library credit to lalit.org",
201 | "homepage": "http://openlss.org",
202 | "keywords": [
203 | "array",
204 | "array conversion",
205 | "xml",
206 | "xml conversion"
207 | ],
208 | "time": "2016-11-10T19:10:18+00:00"
209 | },
210 | {
211 | "name": "php-cs-fixer/diff",
212 | "version": "v1.3.0",
213 | "source": {
214 | "type": "git",
215 | "url": "https://github.com/PHP-CS-Fixer/diff.git",
216 | "reference": "78bb099e9c16361126c86ce82ec4405ebab8e756"
217 | },
218 | "dist": {
219 | "type": "zip",
220 | "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/78bb099e9c16361126c86ce82ec4405ebab8e756",
221 | "reference": "78bb099e9c16361126c86ce82ec4405ebab8e756",
222 | "shasum": ""
223 | },
224 | "require": {
225 | "php": "^5.6 || ^7.0"
226 | },
227 | "require-dev": {
228 | "phpunit/phpunit": "^5.7.23 || ^6.4.3",
229 | "symfony/process": "^3.3"
230 | },
231 | "type": "library",
232 | "autoload": {
233 | "classmap": [
234 | "src/"
235 | ]
236 | },
237 | "notification-url": "https://packagist.org/downloads/",
238 | "license": [
239 | "BSD-3-Clause"
240 | ],
241 | "authors": [
242 | {
243 | "name": "Kore Nordmann",
244 | "email": "mail@kore-nordmann.de"
245 | },
246 | {
247 | "name": "Sebastian Bergmann",
248 | "email": "sebastian@phpunit.de"
249 | },
250 | {
251 | "name": "SpacePossum"
252 | }
253 | ],
254 | "description": "sebastian/diff v2 backport support for PHP5.6",
255 | "homepage": "https://github.com/PHP-CS-Fixer",
256 | "keywords": [
257 | "diff"
258 | ],
259 | "time": "2018-02-15T16:58:55+00:00"
260 | },
261 | {
262 | "name": "psr/log",
263 | "version": "1.1.0",
264 | "source": {
265 | "type": "git",
266 | "url": "https://github.com/php-fig/log.git",
267 | "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
268 | },
269 | "dist": {
270 | "type": "zip",
271 | "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
272 | "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
273 | "shasum": ""
274 | },
275 | "require": {
276 | "php": ">=5.3.0"
277 | },
278 | "type": "library",
279 | "extra": {
280 | "branch-alias": {
281 | "dev-master": "1.0.x-dev"
282 | }
283 | },
284 | "autoload": {
285 | "psr-4": {
286 | "Psr\\Log\\": "Psr/Log/"
287 | }
288 | },
289 | "notification-url": "https://packagist.org/downloads/",
290 | "license": [
291 | "MIT"
292 | ],
293 | "authors": [
294 | {
295 | "name": "PHP-FIG",
296 | "homepage": "http://www.php-fig.org/"
297 | }
298 | ],
299 | "description": "Common interface for logging libraries",
300 | "homepage": "https://github.com/php-fig/log",
301 | "keywords": [
302 | "log",
303 | "psr",
304 | "psr-3"
305 | ],
306 | "time": "2018-11-20T15:27:04+00:00"
307 | },
308 | {
309 | "name": "squizlabs/php_codesniffer",
310 | "version": "3.3.2",
311 | "source": {
312 | "type": "git",
313 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
314 | "reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e"
315 | },
316 | "dist": {
317 | "type": "zip",
318 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/6ad28354c04b364c3c71a34e4a18b629cc3b231e",
319 | "reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e",
320 | "shasum": ""
321 | },
322 | "require": {
323 | "ext-simplexml": "*",
324 | "ext-tokenizer": "*",
325 | "ext-xmlwriter": "*",
326 | "php": ">=5.4.0"
327 | },
328 | "require-dev": {
329 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
330 | },
331 | "bin": [
332 | "bin/phpcs",
333 | "bin/phpcbf"
334 | ],
335 | "type": "library",
336 | "extra": {
337 | "branch-alias": {
338 | "dev-master": "3.x-dev"
339 | }
340 | },
341 | "notification-url": "https://packagist.org/downloads/",
342 | "license": [
343 | "BSD-3-Clause"
344 | ],
345 | "authors": [
346 | {
347 | "name": "Greg Sherwood",
348 | "role": "lead"
349 | }
350 | ],
351 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
352 | "homepage": "http://www.squizlabs.com/php-codesniffer",
353 | "keywords": [
354 | "phpcs",
355 | "standards"
356 | ],
357 | "time": "2018-09-23T23:08:17+00:00"
358 | },
359 | {
360 | "name": "vimeo/psalm",
361 | "version": "1.1.9",
362 | "source": {
363 | "type": "git",
364 | "url": "https://github.com/vimeo/psalm.git",
365 | "reference": "d15cf3b7f50249caf933144c8926c8e69aff3d34"
366 | },
367 | "dist": {
368 | "type": "zip",
369 | "url": "https://api.github.com/repos/vimeo/psalm/zipball/d15cf3b7f50249caf933144c8926c8e69aff3d34",
370 | "reference": "d15cf3b7f50249caf933144c8926c8e69aff3d34",
371 | "shasum": ""
372 | },
373 | "require": {
374 | "composer/xdebug-handler": "^1.1",
375 | "muglug/package-versions-56": "1.2.4",
376 | "nikic/php-parser": "^3.1",
377 | "openlss/lib-array2xml": "^0.0.10||^0.5.1",
378 | "php": "^5.6||^7.0",
379 | "php-cs-fixer/diff": "^1.2"
380 | },
381 | "provide": {
382 | "psalm/psalm": "self.version"
383 | },
384 | "require-dev": {
385 | "bamarni/composer-bin-plugin": "^1.2",
386 | "php-coveralls/php-coveralls": "^2.0",
387 | "phpunit/phpunit": "^5.7.4",
388 | "squizlabs/php_codesniffer": "^3.0"
389 | },
390 | "suggest": {
391 | "ext-igbinary": "^2.0.5"
392 | },
393 | "bin": [
394 | "psalm",
395 | "psalter"
396 | ],
397 | "type": "library",
398 | "extra": {
399 | "branch-alias": {
400 | "dev-master": "2.x-dev",
401 | "dev-1.x": "1.x-dev"
402 | }
403 | },
404 | "autoload": {
405 | "psr-4": {
406 | "Psalm\\": "src/Psalm"
407 | }
408 | },
409 | "notification-url": "https://packagist.org/downloads/",
410 | "license": [
411 | "MIT"
412 | ],
413 | "authors": [
414 | {
415 | "name": "Matthew Brown"
416 | }
417 | ],
418 | "description": "A static analysis tool for finding errors in PHP applications",
419 | "keywords": [
420 | "code",
421 | "inspection",
422 | "php"
423 | ],
424 | "time": "2018-08-14T16:06:16+00:00"
425 | }
426 | ],
427 | "aliases": [],
428 | "minimum-stability": "stable",
429 | "stability-flags": [],
430 | "prefer-stable": false,
431 | "prefer-lowest": false,
432 | "platform": {
433 | "php": "^7.0",
434 | "ext-json": "*",
435 | "ext-reflection": "*"
436 | },
437 | "platform-dev": []
438 | }
439 |
--------------------------------------------------------------------------------
/phpbridge/php-server/Commands.php:
--------------------------------------------------------------------------------
1 | $value) {
63 | if ($key !== 'GLOBALS') {
64 | $result[$key] = $value;
65 | }
66 | }
67 | return $result;
68 | }
69 | return $GLOBALS[$name];
70 | }
71 |
72 | /**
73 | * Set a global variable.
74 | *
75 | * @param string $name
76 | * @param mixed $value
77 | *
78 | * @return null
79 | */
80 | public static function setGlobal(string $name, $value)
81 | {
82 | $GLOBALS[$name] = $value;
83 | return null;
84 | }
85 |
86 | /**
87 | * Call a function.
88 | *
89 | * @param string $name
90 | * @param array $args
91 | *
92 | * @return mixed
93 | */
94 | public static function callFun(string $name, array $args)
95 | {
96 | if (is_callable($name)) {
97 | return $name(...$args);
98 | } elseif (method_exists(NonFunctionProxy::class, $name)) {
99 | return NonFunctionProxy::$name(...$args);
100 | }
101 | throw new \Exception("Could not resolve function '$name'");
102 | }
103 |
104 | /**
105 | * Call an object.
106 | *
107 | * @param callable $obj
108 | * @param array $args
109 | *
110 | * @return mixed
111 | */
112 | public static function callObj($obj, array $args)
113 | {
114 | return $obj(...$args);
115 | }
116 |
117 | /**
118 | * Call an object or class method.
119 | *
120 | * @param object|string $obj
121 | * @param string $name
122 | * @param array $args
123 | *
124 | * @return mixed
125 | */
126 | public static function callMethod($obj, string $name, array $args)
127 | {
128 | $method = [$obj, $name];
129 | return $method(...$args);
130 | }
131 |
132 | /**
133 | * Instantiate an object.
134 | *
135 | * @param string $name
136 | * @param array $args
137 | *
138 | * @return object
139 | */
140 | public static function createObject(string $name, array $args)
141 | {
142 | return new $name(...$args);
143 | }
144 |
145 | /**
146 | * Check whether an offset exists.
147 | *
148 | * @param \ArrayAccess $obj
149 | * @param mixed $offset
150 | *
151 | * @return bool
152 | */
153 | public static function hasItem($obj, $offset): bool
154 | {
155 | return isset($obj[$offset]);
156 | }
157 |
158 | /**
159 | * Get the value at an offset.
160 | *
161 | * @param \ArrayAccess $obj
162 | * @param mixed $offset
163 | *
164 | * @return mixed
165 | */
166 | public static function getItem($obj, $offset)
167 | {
168 | return $obj[$offset];
169 | }
170 |
171 | /**
172 | * Set the value at an offset.
173 | *
174 | * @param \ArrayAccess $obj
175 | * @param mixed $offset
176 | * @param mixed $value
177 | *
178 | * @return null
179 | */
180 | public static function setItem($obj, $offset, $value)
181 | {
182 | $obj[$offset] = $value;
183 | return null;
184 | }
185 |
186 | /**
187 | * Remove the value at an offset.
188 | *
189 | * @param \ArrayAccess $obj
190 | * @param mixed $offset
191 | *
192 | * @return null
193 | */
194 | public static function delItem($obj, $offset)
195 | {
196 | unset($obj[$offset]);
197 | return null;
198 | }
199 |
200 | /**
201 | * Get an object property.
202 | *
203 | * @param object $obj
204 | * @param string $name
205 | *
206 | * @return mixed
207 | */
208 | public static function getProperty($obj, string $name)
209 | {
210 | if (property_exists($obj, $name)) {
211 | return $obj->$name;
212 | } else {
213 | $class = get_class($obj);
214 | throw new AttributeError(
215 | "'$class' object has no property '$name'"
216 | );
217 | }
218 | }
219 |
220 | /**
221 | * Set an object property to a value.
222 | *
223 | * @param object $obj
224 | * @param string $name
225 | * @param mixed $value
226 | *
227 | * @return null
228 | */
229 | public static function setProperty($obj, $name, $value)
230 | {
231 | $obj->$name = $value;
232 | return null;
233 | }
234 |
235 | /**
236 | * Unset an object property.
237 | *
238 | * @param object $obj
239 | * @param string $name
240 | *
241 | * @return null
242 | */
243 | public static function unsetProperty($obj, string $name)
244 | {
245 | if (!property_exists($obj, $name)) {
246 | $class = get_class($obj);
247 | throw new AttributeError(
248 | "'$class' object has no property '$name'"
249 | );
250 | }
251 | unset($obj->$name);
252 | return null;
253 | }
254 |
255 | /**
256 | * Get an array of all property names.
257 | *
258 | * Doesn't work for all objects. For example, properties set on an
259 | * ArrayObject don't show up. They don't show up either when the object
260 | * is cast to an array, so that's probably related.
261 | *
262 | * @param object $obj
263 | *
264 | * @return array
265 | */
266 | public static function listNonDefaultProperties($obj)
267 | {
268 | $properties = [];
269 | foreach ((new \ReflectionObject($obj))->getProperties() as $property) {
270 | if ($property->isPublic() && !$property->isDefault()) {
271 | $properties[] = $property->getName();
272 | }
273 | }
274 | return $properties;
275 | }
276 |
277 | /**
278 | * Get a summary of a class
279 | *
280 | * @param string $class
281 | *
282 | * @return array
283 | */
284 | public static function classInfo(string $class): array
285 | {
286 | $reflectionClass = new \ReflectionClass($class);
287 | $parent = $reflectionClass->getParentClass();
288 | if ($parent !== false) {
289 | $parent = $parent->getName();
290 | }
291 | $info = [
292 | 'name' => $reflectionClass->getName(),
293 | 'doc' => $reflectionClass->getDocComment(),
294 | 'consts' => [],
295 | 'methods' => [],
296 | 'properties' => [],
297 | 'interfaces' => $reflectionClass->getInterfaceNames(),
298 | 'traits' => $reflectionClass->getTraitNames(),
299 | 'isAbstract' => $reflectionClass->isAbstract(),
300 | 'isInterface' => $reflectionClass->isInterface(),
301 | 'isTrait' => $reflectionClass->isTrait(),
302 | 'parent' => $parent
303 | ];
304 |
305 | foreach ($reflectionClass->getReflectionConstants() as $constant) {
306 | if ($constant->isPublic()) {
307 | $info['consts'][$constant->getName()] = $constant->getValue();
308 | }
309 | }
310 |
311 | foreach ($reflectionClass->getMethods() as $method) {
312 | if ($method->isPublic()) {
313 | $info['methods'][$method->getName()] = [
314 | 'static' => $method->isStatic(),
315 | 'doc' => $method->getDocComment(),
316 | 'params' => array_map(
317 | [static::class, 'paramInfo'],
318 | $method->getParameters()
319 | ),
320 | 'returnType' => static::typeInfo($method->getReturnType()),
321 | 'owner' => $method->getDeclaringClass()->getName(),
322 | 'isConstructor' => $method->isConstructor()
323 | ];
324 | }
325 | }
326 |
327 | $defaults = $reflectionClass->getDefaultProperties();
328 | foreach ($reflectionClass->getProperties() as $property) {
329 | if ($property->isPublic()) {
330 | $name = $property->getName();
331 | $info['properties'][$name] = [
332 | 'default' => array_key_exists($name, $defaults)
333 | ? $defaults[$name] : null,
334 | 'doc' => $property->getDocComment()
335 | ];
336 | }
337 | }
338 |
339 | return $info;
340 | }
341 |
342 | /**
343 | * Get detailed information about a function.
344 | *
345 | * @param string $name
346 | *
347 | * @return array
348 | */
349 | public static function funcInfo(string $name): array
350 | {
351 | if (is_callable($name) && is_string($name)) {
352 | $function = new \ReflectionFunction($name);
353 | } elseif (method_exists(NonFunctionProxy::class, $name)) {
354 | $function = (new \ReflectionClass(NonFunctionProxy::class))
355 | ->getMethod($name);
356 | } else {
357 | throw new \Exception("Could not resolve function '$name'");
358 | }
359 | return [
360 | 'name' => $function->getName(),
361 | 'doc' => $function->getDocComment(),
362 | 'params' => array_map(
363 | [static::class, 'paramInfo'],
364 | $function->getParameters()
365 | ),
366 | 'returnType' => static::typeInfo($function->getReturnType())
367 | ];
368 | }
369 |
370 | /**
371 | * Serialize information about a function parameter.
372 | *
373 | * @param \ReflectionParameter $parameter
374 | *
375 | * @return array
376 | */
377 | private static function paramInfo(\ReflectionParameter $parameter): array
378 | {
379 | $hasDefault = $parameter->isDefaultValueAvailable();
380 | return [
381 | 'name' => $parameter->getName(),
382 | 'type' => static::typeInfo($parameter->getType()),
383 | 'hasDefault' => $hasDefault,
384 | 'default' => $hasDefault ? $parameter->getDefaultValue() : null,
385 | 'variadic' => $parameter->isVariadic(),
386 | 'isOptional' => $parameter->isOptional()
387 | ];
388 | }
389 |
390 | /**
391 | * Serialize information about a parameter or return type.
392 | *
393 | * @param \ReflectionType|null $type
394 | *
395 | * @return array|null
396 | */
397 | private static function typeInfo(\ReflectionType $type = null)
398 | {
399 | if ($type === null) {
400 | return null;
401 | }
402 | return [
403 | 'name' => $type->getName(),
404 | 'isClass' => !$type->isBuiltin(),
405 | 'nullable' => $type->allowsNull(),
406 | ];
407 | }
408 |
409 | /**
410 | * Get an array of all defined constant names.
411 | *
412 | * @return array
413 | */
414 | public static function listConsts(): array
415 | {
416 | return array_keys(get_defined_constants());
417 | }
418 |
419 | /**
420 | * Get an array of all global variable names.
421 | *
422 | * @return array
423 | */
424 | public static function listGlobals(): array
425 | {
426 | return array_keys($GLOBALS);
427 | }
428 |
429 | /**
430 | * Get an array of names of all defined functions.
431 | *
432 | * @return array
433 | */
434 | public static function listFuns(): array
435 | {
436 | $result = get_class_methods(NonFunctionProxy::class);
437 | foreach (get_defined_functions() as $functions) {
438 | $result = array_merge($result, $functions);
439 | }
440 | return $result;
441 | }
442 |
443 | /**
444 | * Get an array of names of all declared classes.
445 | *
446 | * @return array
447 | */
448 | public static function listClasses(): array
449 | {
450 | return array_merge(get_declared_classes(), get_declared_interfaces());
451 | }
452 |
453 | /**
454 | * List all resolvable names.
455 | *
456 | * @param string $namespace
457 | *
458 | * @return \Generator
459 | */
460 | public static function listEverything(string $namespace = ''): \Generator
461 | {
462 | $prefix = $namespace === '' ? '' : "$namespace\\";
463 | $names = array_merge(
464 | static::listConsts(),
465 | static::listFuns(),
466 | static::listClasses(),
467 | static::listGlobals()
468 | );
469 | if ($prefix === '') {
470 | yield from $names;
471 | return;
472 | }
473 | foreach ($names as $name) {
474 | if (substr($name, 0, strlen($prefix)) === $prefix) {
475 | yield substr($name, strlen($prefix));
476 | }
477 | }
478 | }
479 |
480 | /**
481 | * List all known (sub)namespaces.
482 | *
483 | * @param string $namespace
484 | *
485 | * @return array
486 | */
487 | public static function listNamespaces(string $namespace = ''): array
488 | {
489 | // We'll use this as a set by only using the keys
490 | $namespaces = [];
491 | foreach (static::listEverything($namespace) as $name) {
492 | if (strpos($name, '\\') !== false) {
493 | $namespaces[explode('\\', $name)[0]] = null;
494 | }
495 | }
496 | return array_keys($namespaces);
497 | }
498 |
499 | /**
500 | * Try to guess what a name represents.
501 | *
502 | * @param string $name
503 | *
504 | * @return string
505 | */
506 | public static function resolveName(string $name): string
507 | {
508 | if (defined($name)) {
509 | return 'const';
510 | } elseif (function_exists($name) ||
511 | method_exists(NonFunctionProxy::class, $name)) {
512 | return 'func';
513 | } elseif (class_exists($name) || interface_exists($name) ||
514 | trait_exists($name)) {
515 | return 'class';
516 | } elseif (array_key_exists($name, $GLOBALS)) {
517 | return 'global';
518 | } else {
519 | return 'none';
520 | }
521 | }
522 |
523 | /**
524 | * Build a string representation for Python reprs using Representer.
525 | *
526 | * @param mixed $value
527 | *
528 | * @return string
529 | */
530 | public static function repr($value): string
531 | {
532 | return (new PythonRepresenter)->repr($value);
533 | }
534 |
535 | /**
536 | * Cast a value to a string.
537 | *
538 | * @param mixed $value
539 | *
540 | * @return string
541 | */
542 | public static function str($value): string
543 | {
544 | return (string)$value;
545 | }
546 |
547 | /**
548 | * Get the count/length of an object.
549 | *
550 | * @param \Countable $value
551 | *
552 | * @return int
553 | */
554 | public static function count(\Countable $value): int
555 | {
556 | return count($value);
557 | }
558 |
559 | /**
560 | * Start iterating over something.
561 | *
562 | * We deliberately return the Generator object so we can get values out
563 | * of it in further commands.
564 | *
565 | * @param iterable $iterable
566 | *
567 | * @return \Generator
568 | */
569 | public static function startIteration($iterable): \Generator
570 | {
571 | /** @psalm-suppress RedundantConditionGivenDocblockType */
572 | if (!(is_array($iterable) || $iterable instanceof \Traversable)) {
573 | if (is_object($iterable)) {
574 | $class = get_class($iterable);
575 | throw new \TypeError("'$class' object is not iterable");
576 | }
577 | $type = gettype($iterable);
578 | throw new \TypeError("'$type' value is not iterable");
579 | }
580 | foreach ($iterable as $key => $value) {
581 | yield $key => $value;
582 | }
583 | }
584 |
585 | /**
586 | * Get the next key and value from a generator.
587 | *
588 | * Returns an array containing a bool indicating whether the generator is
589 | * still going, the new key, and the new value.
590 | *
591 | * @param \Generator $generator
592 | *
593 | * @return array
594 | */
595 | public static function nextIteration(\Generator $generator): array
596 | {
597 | $ret = [$generator->valid(), $generator->key(), $generator->current()];
598 | $generator->next();
599 | return $ret;
600 | }
601 |
602 | /**
603 | * Throw an exception. Used for throwing an error while receiving a command.
604 | *
605 | * @param string $class
606 | * @param string $message
607 | * @return void
608 | */
609 | public static function throwException(string $class, string $message)
610 | {
611 | $obj = new $class($message);
612 | if (!$obj instanceof \Throwable) {
613 | throw new \RuntimeException(
614 | "Can't throw '$class' with message '$message' - " .
615 | "not throwable"
616 | );
617 | }
618 | throw $obj;
619 | }
620 | }
621 |
--------------------------------------------------------------------------------