Completed
Push — master ( 3f5c2c...6a0d4f )
by
unknown
48:24 queued 17:14
created
lib/private/AppFramework/Http/Dispatcher.php 2 patches
Indentation   +224 added lines, -224 removed lines patch added patch discarded remove patch
@@ -26,228 +26,228 @@
 block discarded – undo
26 26
  * Class to dispatch the request to the middleware dispatcher
27 27
  */
28 28
 class Dispatcher {
29
-	/** @var MiddlewareDispatcher */
30
-	private $middlewareDispatcher;
31
-
32
-	/** @var Http */
33
-	private $protocol;
34
-
35
-	/** @var ControllerMethodReflector */
36
-	private $reflector;
37
-
38
-	/** @var IRequest */
39
-	private $request;
40
-
41
-	/** @var IConfig */
42
-	private $config;
43
-
44
-	/** @var ConnectionAdapter */
45
-	private $connection;
46
-
47
-	/** @var LoggerInterface */
48
-	private $logger;
49
-
50
-	/** @var IEventLogger */
51
-	private $eventLogger;
52
-
53
-	private ContainerInterface $appContainer;
54
-
55
-	/**
56
-	 * @param Http $protocol the http protocol with contains all status headers
57
-	 * @param MiddlewareDispatcher $middlewareDispatcher the dispatcher which
58
-	 *                                                   runs the middleware
59
-	 * @param ControllerMethodReflector $reflector the reflector that is used to inject
60
-	 *                                             the arguments for the controller
61
-	 * @param IRequest $request the incoming request
62
-	 * @param IConfig $config
63
-	 * @param ConnectionAdapter $connection
64
-	 * @param LoggerInterface $logger
65
-	 * @param IEventLogger $eventLogger
66
-	 */
67
-	public function __construct(
68
-		Http $protocol,
69
-		MiddlewareDispatcher $middlewareDispatcher,
70
-		ControllerMethodReflector $reflector,
71
-		IRequest $request,
72
-		IConfig $config,
73
-		ConnectionAdapter $connection,
74
-		LoggerInterface $logger,
75
-		IEventLogger $eventLogger,
76
-		ContainerInterface $appContainer,
77
-	) {
78
-		$this->protocol = $protocol;
79
-		$this->middlewareDispatcher = $middlewareDispatcher;
80
-		$this->reflector = $reflector;
81
-		$this->request = $request;
82
-		$this->config = $config;
83
-		$this->connection = $connection;
84
-		$this->logger = $logger;
85
-		$this->eventLogger = $eventLogger;
86
-		$this->appContainer = $appContainer;
87
-	}
88
-
89
-
90
-	/**
91
-	 * Handles a request and calls the dispatcher on the controller
92
-	 * @param Controller $controller the controller which will be called
93
-	 * @param string $methodName the method name which will be called on
94
-	 *                           the controller
95
-	 * @return array $array[0] contains the http status header as a string,
96
-	 *               $array[1] contains response headers as an array,
97
-	 *               $array[2] contains response cookies as an array,
98
-	 *               $array[3] contains the response output as a string,
99
-	 *               $array[4] contains the response object
100
-	 * @throws \Exception
101
-	 */
102
-	public function dispatch(Controller $controller, string $methodName): array {
103
-		$out = [null, [], null];
104
-
105
-		try {
106
-			// prefill reflector with everything that's needed for the
107
-			// middlewares
108
-			$this->reflector->reflect($controller, $methodName);
109
-
110
-			$this->middlewareDispatcher->beforeController($controller,
111
-				$methodName);
112
-
113
-			$databaseStatsBefore = [];
114
-			if ($this->config->getSystemValueBool('debug', false)) {
115
-				$databaseStatsBefore = $this->connection->getInner()->getStats();
116
-			}
117
-
118
-			$response = $this->executeController($controller, $methodName);
119
-
120
-			if (!empty($databaseStatsBefore)) {
121
-				$databaseStatsAfter = $this->connection->getInner()->getStats();
122
-				$numBuilt = $databaseStatsAfter['built'] - $databaseStatsBefore['built'];
123
-				$numExecuted = $databaseStatsAfter['executed'] - $databaseStatsBefore['executed'];
124
-
125
-				if ($numBuilt > 50) {
126
-					$this->logger->debug('Controller {class}::{method} created {count} QueryBuilder objects, please check if they are created inside a loop by accident.', [
127
-						'class' => get_class($controller),
128
-						'method' => $methodName,
129
-						'count' => $numBuilt,
130
-					]);
131
-				}
132
-
133
-				if ($numExecuted > 100) {
134
-					$this->logger->warning('Controller {class}::{method} executed {count} queries.', [
135
-						'class' => get_class($controller),
136
-						'method' => $methodName,
137
-						'count' => $numExecuted,
138
-					]);
139
-				}
140
-			}
141
-
142
-			// if an exception appears, the middleware checks if it can handle the
143
-			// exception and creates a response. If no response is created, it is
144
-			// assumed that there's no middleware who can handle it and the error is
145
-			// thrown again
146
-		} catch (\Exception $exception) {
147
-			$response = $this->middlewareDispatcher->afterException(
148
-				$controller, $methodName, $exception);
149
-		} catch (\Throwable $throwable) {
150
-			$exception = new \Exception($throwable->getMessage() . ' in file \'' . $throwable->getFile() . '\' line ' . $throwable->getLine(), $throwable->getCode(), $throwable);
151
-			$response = $this->middlewareDispatcher->afterException(
152
-				$controller, $methodName, $exception);
153
-		}
154
-
155
-		$response = $this->middlewareDispatcher->afterController(
156
-			$controller, $methodName, $response);
157
-
158
-		// depending on the cache object the headers need to be changed
159
-		$out[0] = $this->protocol->getStatusHeader($response->getStatus());
160
-		$out[1] = array_merge($response->getHeaders());
161
-		$out[2] = $response->getCookies();
162
-		$out[3] = $this->middlewareDispatcher->beforeOutput(
163
-			$controller, $methodName, $response->render()
164
-		);
165
-		$out[4] = $response;
166
-
167
-		return $out;
168
-	}
169
-
170
-
171
-	/**
172
-	 * Uses the reflected parameters, types and request parameters to execute
173
-	 * the controller
174
-	 * @param Controller $controller the controller to be executed
175
-	 * @param string $methodName the method on the controller that should be executed
176
-	 * @return Response
177
-	 */
178
-	private function executeController(Controller $controller, string $methodName): Response {
179
-		$arguments = [];
180
-
181
-		// valid types that will be cast
182
-		$types = ['int', 'integer', 'bool', 'boolean', 'float', 'double'];
183
-
184
-		foreach ($this->reflector->getParameters() as $param => $default) {
185
-			// try to get the parameter from the request object and cast
186
-			// it to the type annotated in the @param annotation
187
-			$value = $this->request->getParam($param, $default);
188
-			$type = $this->reflector->getType($param);
189
-
190
-			// Converted the string `'false'` to false when the controller wants a boolean
191
-			if ($value === 'false' && ($type === 'bool' || $type === 'boolean')) {
192
-				$value = false;
193
-			} elseif ($value !== null && \in_array($type, $types, true)) {
194
-				settype($value, $type);
195
-				$this->ensureParameterValueSatisfiesRange($param, $value);
196
-			} elseif ($value === null && $type !== null && $this->appContainer->has($type)) {
197
-				$value = $this->appContainer->get($type);
198
-			}
199
-
200
-			$arguments[] = $value;
201
-		}
202
-
203
-		$this->eventLogger->start('controller:' . get_class($controller) . '::' . $methodName, 'App framework controller execution');
204
-		try {
205
-			$response = \call_user_func_array([$controller, $methodName], $arguments);
206
-		} catch (\TypeError $e) {
207
-			// Only intercept TypeErrors occuring on the first line, meaning that the invocation of the controller method failed.
208
-			// Any other TypeError happens inside the controller method logic and should be logged as normal.
209
-			if ($e->getFile() === $this->reflector->getFile() && $e->getLine() === $this->reflector->getStartLine()) {
210
-				$this->logger->debug('Failed to call controller method: ' . $e->getMessage(), ['exception' => $e]);
211
-				return new Response(Http::STATUS_BAD_REQUEST);
212
-			}
213
-
214
-			throw $e;
215
-		}
216
-		$this->eventLogger->end('controller:' . get_class($controller) . '::' . $methodName);
217
-
218
-		if (!($response instanceof Response)) {
219
-			$this->logger->debug($controller::class . '::' . $methodName . ' returned raw data. Please wrap it in a Response or one of it\'s inheritors.');
220
-		}
221
-
222
-		// format response
223
-		if ($response instanceof DataResponse || !($response instanceof Response)) {
224
-			$format = $this->request->getFormat();
225
-
226
-			if ($format !== null) {
227
-				$response = $controller->buildResponse($response, $format);
228
-			} else {
229
-				$response = $controller->buildResponse($response);
230
-			}
231
-		}
232
-
233
-		return $response;
234
-	}
235
-
236
-	/**
237
-	 * @psalm-param mixed $value
238
-	 * @throws ParameterOutOfRangeException
239
-	 */
240
-	private function ensureParameterValueSatisfiesRange(string $param, $value): void {
241
-		$rangeInfo = $this->reflector->getRange($param);
242
-		if ($rangeInfo) {
243
-			if ($value < $rangeInfo['min'] || $value > $rangeInfo['max']) {
244
-				throw new ParameterOutOfRangeException(
245
-					$param,
246
-					$value,
247
-					$rangeInfo['min'],
248
-					$rangeInfo['max'],
249
-				);
250
-			}
251
-		}
252
-	}
29
+    /** @var MiddlewareDispatcher */
30
+    private $middlewareDispatcher;
31
+
32
+    /** @var Http */
33
+    private $protocol;
34
+
35
+    /** @var ControllerMethodReflector */
36
+    private $reflector;
37
+
38
+    /** @var IRequest */
39
+    private $request;
40
+
41
+    /** @var IConfig */
42
+    private $config;
43
+
44
+    /** @var ConnectionAdapter */
45
+    private $connection;
46
+
47
+    /** @var LoggerInterface */
48
+    private $logger;
49
+
50
+    /** @var IEventLogger */
51
+    private $eventLogger;
52
+
53
+    private ContainerInterface $appContainer;
54
+
55
+    /**
56
+     * @param Http $protocol the http protocol with contains all status headers
57
+     * @param MiddlewareDispatcher $middlewareDispatcher the dispatcher which
58
+     *                                                   runs the middleware
59
+     * @param ControllerMethodReflector $reflector the reflector that is used to inject
60
+     *                                             the arguments for the controller
61
+     * @param IRequest $request the incoming request
62
+     * @param IConfig $config
63
+     * @param ConnectionAdapter $connection
64
+     * @param LoggerInterface $logger
65
+     * @param IEventLogger $eventLogger
66
+     */
67
+    public function __construct(
68
+        Http $protocol,
69
+        MiddlewareDispatcher $middlewareDispatcher,
70
+        ControllerMethodReflector $reflector,
71
+        IRequest $request,
72
+        IConfig $config,
73
+        ConnectionAdapter $connection,
74
+        LoggerInterface $logger,
75
+        IEventLogger $eventLogger,
76
+        ContainerInterface $appContainer,
77
+    ) {
78
+        $this->protocol = $protocol;
79
+        $this->middlewareDispatcher = $middlewareDispatcher;
80
+        $this->reflector = $reflector;
81
+        $this->request = $request;
82
+        $this->config = $config;
83
+        $this->connection = $connection;
84
+        $this->logger = $logger;
85
+        $this->eventLogger = $eventLogger;
86
+        $this->appContainer = $appContainer;
87
+    }
88
+
89
+
90
+    /**
91
+     * Handles a request and calls the dispatcher on the controller
92
+     * @param Controller $controller the controller which will be called
93
+     * @param string $methodName the method name which will be called on
94
+     *                           the controller
95
+     * @return array $array[0] contains the http status header as a string,
96
+     *               $array[1] contains response headers as an array,
97
+     *               $array[2] contains response cookies as an array,
98
+     *               $array[3] contains the response output as a string,
99
+     *               $array[4] contains the response object
100
+     * @throws \Exception
101
+     */
102
+    public function dispatch(Controller $controller, string $methodName): array {
103
+        $out = [null, [], null];
104
+
105
+        try {
106
+            // prefill reflector with everything that's needed for the
107
+            // middlewares
108
+            $this->reflector->reflect($controller, $methodName);
109
+
110
+            $this->middlewareDispatcher->beforeController($controller,
111
+                $methodName);
112
+
113
+            $databaseStatsBefore = [];
114
+            if ($this->config->getSystemValueBool('debug', false)) {
115
+                $databaseStatsBefore = $this->connection->getInner()->getStats();
116
+            }
117
+
118
+            $response = $this->executeController($controller, $methodName);
119
+
120
+            if (!empty($databaseStatsBefore)) {
121
+                $databaseStatsAfter = $this->connection->getInner()->getStats();
122
+                $numBuilt = $databaseStatsAfter['built'] - $databaseStatsBefore['built'];
123
+                $numExecuted = $databaseStatsAfter['executed'] - $databaseStatsBefore['executed'];
124
+
125
+                if ($numBuilt > 50) {
126
+                    $this->logger->debug('Controller {class}::{method} created {count} QueryBuilder objects, please check if they are created inside a loop by accident.', [
127
+                        'class' => get_class($controller),
128
+                        'method' => $methodName,
129
+                        'count' => $numBuilt,
130
+                    ]);
131
+                }
132
+
133
+                if ($numExecuted > 100) {
134
+                    $this->logger->warning('Controller {class}::{method} executed {count} queries.', [
135
+                        'class' => get_class($controller),
136
+                        'method' => $methodName,
137
+                        'count' => $numExecuted,
138
+                    ]);
139
+                }
140
+            }
141
+
142
+            // if an exception appears, the middleware checks if it can handle the
143
+            // exception and creates a response. If no response is created, it is
144
+            // assumed that there's no middleware who can handle it and the error is
145
+            // thrown again
146
+        } catch (\Exception $exception) {
147
+            $response = $this->middlewareDispatcher->afterException(
148
+                $controller, $methodName, $exception);
149
+        } catch (\Throwable $throwable) {
150
+            $exception = new \Exception($throwable->getMessage() . ' in file \'' . $throwable->getFile() . '\' line ' . $throwable->getLine(), $throwable->getCode(), $throwable);
151
+            $response = $this->middlewareDispatcher->afterException(
152
+                $controller, $methodName, $exception);
153
+        }
154
+
155
+        $response = $this->middlewareDispatcher->afterController(
156
+            $controller, $methodName, $response);
157
+
158
+        // depending on the cache object the headers need to be changed
159
+        $out[0] = $this->protocol->getStatusHeader($response->getStatus());
160
+        $out[1] = array_merge($response->getHeaders());
161
+        $out[2] = $response->getCookies();
162
+        $out[3] = $this->middlewareDispatcher->beforeOutput(
163
+            $controller, $methodName, $response->render()
164
+        );
165
+        $out[4] = $response;
166
+
167
+        return $out;
168
+    }
169
+
170
+
171
+    /**
172
+     * Uses the reflected parameters, types and request parameters to execute
173
+     * the controller
174
+     * @param Controller $controller the controller to be executed
175
+     * @param string $methodName the method on the controller that should be executed
176
+     * @return Response
177
+     */
178
+    private function executeController(Controller $controller, string $methodName): Response {
179
+        $arguments = [];
180
+
181
+        // valid types that will be cast
182
+        $types = ['int', 'integer', 'bool', 'boolean', 'float', 'double'];
183
+
184
+        foreach ($this->reflector->getParameters() as $param => $default) {
185
+            // try to get the parameter from the request object and cast
186
+            // it to the type annotated in the @param annotation
187
+            $value = $this->request->getParam($param, $default);
188
+            $type = $this->reflector->getType($param);
189
+
190
+            // Converted the string `'false'` to false when the controller wants a boolean
191
+            if ($value === 'false' && ($type === 'bool' || $type === 'boolean')) {
192
+                $value = false;
193
+            } elseif ($value !== null && \in_array($type, $types, true)) {
194
+                settype($value, $type);
195
+                $this->ensureParameterValueSatisfiesRange($param, $value);
196
+            } elseif ($value === null && $type !== null && $this->appContainer->has($type)) {
197
+                $value = $this->appContainer->get($type);
198
+            }
199
+
200
+            $arguments[] = $value;
201
+        }
202
+
203
+        $this->eventLogger->start('controller:' . get_class($controller) . '::' . $methodName, 'App framework controller execution');
204
+        try {
205
+            $response = \call_user_func_array([$controller, $methodName], $arguments);
206
+        } catch (\TypeError $e) {
207
+            // Only intercept TypeErrors occuring on the first line, meaning that the invocation of the controller method failed.
208
+            // Any other TypeError happens inside the controller method logic and should be logged as normal.
209
+            if ($e->getFile() === $this->reflector->getFile() && $e->getLine() === $this->reflector->getStartLine()) {
210
+                $this->logger->debug('Failed to call controller method: ' . $e->getMessage(), ['exception' => $e]);
211
+                return new Response(Http::STATUS_BAD_REQUEST);
212
+            }
213
+
214
+            throw $e;
215
+        }
216
+        $this->eventLogger->end('controller:' . get_class($controller) . '::' . $methodName);
217
+
218
+        if (!($response instanceof Response)) {
219
+            $this->logger->debug($controller::class . '::' . $methodName . ' returned raw data. Please wrap it in a Response or one of it\'s inheritors.');
220
+        }
221
+
222
+        // format response
223
+        if ($response instanceof DataResponse || !($response instanceof Response)) {
224
+            $format = $this->request->getFormat();
225
+
226
+            if ($format !== null) {
227
+                $response = $controller->buildResponse($response, $format);
228
+            } else {
229
+                $response = $controller->buildResponse($response);
230
+            }
231
+        }
232
+
233
+        return $response;
234
+    }
235
+
236
+    /**
237
+     * @psalm-param mixed $value
238
+     * @throws ParameterOutOfRangeException
239
+     */
240
+    private function ensureParameterValueSatisfiesRange(string $param, $value): void {
241
+        $rangeInfo = $this->reflector->getRange($param);
242
+        if ($rangeInfo) {
243
+            if ($value < $rangeInfo['min'] || $value > $rangeInfo['max']) {
244
+                throw new ParameterOutOfRangeException(
245
+                    $param,
246
+                    $value,
247
+                    $rangeInfo['min'],
248
+                    $rangeInfo['max'],
249
+                );
250
+            }
251
+        }
252
+    }
253 253
 }
Please login to merge, or discard this patch.
Spacing   +5 added lines, -5 removed lines patch added patch discarded remove patch
@@ -147,7 +147,7 @@  discard block
 block discarded – undo
147 147
 			$response = $this->middlewareDispatcher->afterException(
148 148
 				$controller, $methodName, $exception);
149 149
 		} catch (\Throwable $throwable) {
150
-			$exception = new \Exception($throwable->getMessage() . ' in file \'' . $throwable->getFile() . '\' line ' . $throwable->getLine(), $throwable->getCode(), $throwable);
150
+			$exception = new \Exception($throwable->getMessage().' in file \''.$throwable->getFile().'\' line '.$throwable->getLine(), $throwable->getCode(), $throwable);
151 151
 			$response = $this->middlewareDispatcher->afterException(
152 152
 				$controller, $methodName, $exception);
153 153
 		}
@@ -200,23 +200,23 @@  discard block
 block discarded – undo
200 200
 			$arguments[] = $value;
201 201
 		}
202 202
 
203
-		$this->eventLogger->start('controller:' . get_class($controller) . '::' . $methodName, 'App framework controller execution');
203
+		$this->eventLogger->start('controller:'.get_class($controller).'::'.$methodName, 'App framework controller execution');
204 204
 		try {
205 205
 			$response = \call_user_func_array([$controller, $methodName], $arguments);
206 206
 		} catch (\TypeError $e) {
207 207
 			// Only intercept TypeErrors occuring on the first line, meaning that the invocation of the controller method failed.
208 208
 			// Any other TypeError happens inside the controller method logic and should be logged as normal.
209 209
 			if ($e->getFile() === $this->reflector->getFile() && $e->getLine() === $this->reflector->getStartLine()) {
210
-				$this->logger->debug('Failed to call controller method: ' . $e->getMessage(), ['exception' => $e]);
210
+				$this->logger->debug('Failed to call controller method: '.$e->getMessage(), ['exception' => $e]);
211 211
 				return new Response(Http::STATUS_BAD_REQUEST);
212 212
 			}
213 213
 
214 214
 			throw $e;
215 215
 		}
216
-		$this->eventLogger->end('controller:' . get_class($controller) . '::' . $methodName);
216
+		$this->eventLogger->end('controller:'.get_class($controller).'::'.$methodName);
217 217
 
218 218
 		if (!($response instanceof Response)) {
219
-			$this->logger->debug($controller::class . '::' . $methodName . ' returned raw data. Please wrap it in a Response or one of it\'s inheritors.');
219
+			$this->logger->debug($controller::class.'::'.$methodName.' returned raw data. Please wrap it in a Response or one of it\'s inheritors.');
220 220
 		}
221 221
 
222 222
 		// format response
Please login to merge, or discard this patch.
lib/private/AppFramework/Utility/ControllerMethodReflector.php 1 patch
Indentation   +133 added lines, -133 removed lines patch added patch discarded remove patch
@@ -14,137 +14,137 @@
 block discarded – undo
14 14
  * Reads and parses annotations from doc comments
15 15
  */
16 16
 class ControllerMethodReflector implements IControllerMethodReflector {
17
-	public $annotations = [];
18
-	private $types = [];
19
-	private $parameters = [];
20
-	private array $ranges = [];
21
-	private int $startLine = 0;
22
-	private string $file = '';
23
-
24
-	/**
25
-	 * @param object $object an object or classname
26
-	 * @param string $method the method which we want to inspect
27
-	 */
28
-	public function reflect($object, string $method) {
29
-		$reflection = new \ReflectionMethod($object, $method);
30
-		$this->startLine = $reflection->getStartLine();
31
-		$this->file = $reflection->getFileName();
32
-
33
-		$docs = $reflection->getDocComment();
34
-
35
-		if ($docs !== false) {
36
-			// extract everything prefixed by @ and first letter uppercase
37
-			preg_match_all('/^\h+\*\h+@(?P<annotation>[A-Z]\w+)((?P<parameter>.*))?$/m', $docs, $matches);
38
-			foreach ($matches['annotation'] as $key => $annotation) {
39
-				$annotation = strtolower($annotation);
40
-				$annotationValue = $matches['parameter'][$key];
41
-				if (str_starts_with($annotationValue, '(') && str_ends_with($annotationValue, ')')) {
42
-					$cutString = substr($annotationValue, 1, -1);
43
-					$cutString = str_replace(' ', '', $cutString);
44
-					$splitArray = explode(',', $cutString);
45
-					foreach ($splitArray as $annotationValues) {
46
-						[$key, $value] = explode('=', $annotationValues);
47
-						$this->annotations[$annotation][$key] = $value;
48
-					}
49
-					continue;
50
-				}
51
-
52
-				$this->annotations[$annotation] = [$annotationValue];
53
-			}
54
-
55
-			// extract type parameter information
56
-			preg_match_all('/@param\h+(?P<type>\w+)\h+\$(?P<var>\w+)/', $docs, $matches);
57
-			$this->types = array_combine($matches['var'], $matches['type']);
58
-			preg_match_all('/@psalm-param\h+(\?)?(?P<type>\w+)<(?P<rangeMin>(-?\d+|min)),\h*(?P<rangeMax>(-?\d+|max))>(\|null)?\h+\$(?P<var>\w+)/', $docs, $matches);
59
-			foreach ($matches['var'] as $index => $varName) {
60
-				if ($matches['type'][$index] !== 'int') {
61
-					// only int ranges are possible at the moment
62
-					// @see https://psalm.dev/docs/annotating_code/type_syntax/scalar_types
63
-					continue;
64
-				}
65
-				$this->ranges[$varName] = [
66
-					'min' => $matches['rangeMin'][$index] === 'min' ? PHP_INT_MIN : (int)$matches['rangeMin'][$index],
67
-					'max' => $matches['rangeMax'][$index] === 'max' ? PHP_INT_MAX : (int)$matches['rangeMax'][$index],
68
-				];
69
-			}
70
-		}
71
-
72
-		foreach ($reflection->getParameters() as $param) {
73
-			// extract type information from PHP 7 scalar types and prefer them over phpdoc annotations
74
-			$type = $param->getType();
75
-			if ($type instanceof \ReflectionNamedType) {
76
-				$this->types[$param->getName()] = $type->getName();
77
-			}
78
-
79
-			$default = null;
80
-			if ($param->isOptional()) {
81
-				$default = $param->getDefaultValue();
82
-			}
83
-			$this->parameters[$param->name] = $default;
84
-		}
85
-	}
86
-
87
-	/**
88
-	 * Inspects the PHPDoc parameters for types
89
-	 * @param string $parameter the parameter whose type comments should be
90
-	 *                          parsed
91
-	 * @return string|null type in the type parameters (@param int $something)
92
-	 *                     would return int or null if not existing
93
-	 */
94
-	public function getType(string $parameter) {
95
-		if (array_key_exists($parameter, $this->types)) {
96
-			return $this->types[$parameter];
97
-		}
98
-
99
-		return null;
100
-	}
101
-
102
-	public function getRange(string $parameter): ?array {
103
-		if (array_key_exists($parameter, $this->ranges)) {
104
-			return $this->ranges[$parameter];
105
-		}
106
-
107
-		return null;
108
-	}
109
-
110
-	/**
111
-	 * @return array the arguments of the method with key => default value
112
-	 */
113
-	public function getParameters(): array {
114
-		return $this->parameters;
115
-	}
116
-
117
-	/**
118
-	 * Check if a method contains an annotation
119
-	 * @param string $name the name of the annotation
120
-	 * @return bool true if the annotation is found
121
-	 */
122
-	public function hasAnnotation(string $name): bool {
123
-		$name = strtolower($name);
124
-		return array_key_exists($name, $this->annotations);
125
-	}
126
-
127
-	/**
128
-	 * Get optional annotation parameter by key
129
-	 *
130
-	 * @param string $name the name of the annotation
131
-	 * @param string $key the string of the annotation
132
-	 * @return string
133
-	 */
134
-	public function getAnnotationParameter(string $name, string $key): string {
135
-		$name = strtolower($name);
136
-		if (isset($this->annotations[$name][$key])) {
137
-			return $this->annotations[$name][$key];
138
-		}
139
-
140
-		return '';
141
-	}
142
-
143
-	public function getStartLine(): int {
144
-		return $this->startLine;
145
-	}
146
-
147
-	public function getFile(): string {
148
-		return $this->file;
149
-	}
17
+    public $annotations = [];
18
+    private $types = [];
19
+    private $parameters = [];
20
+    private array $ranges = [];
21
+    private int $startLine = 0;
22
+    private string $file = '';
23
+
24
+    /**
25
+     * @param object $object an object or classname
26
+     * @param string $method the method which we want to inspect
27
+     */
28
+    public function reflect($object, string $method) {
29
+        $reflection = new \ReflectionMethod($object, $method);
30
+        $this->startLine = $reflection->getStartLine();
31
+        $this->file = $reflection->getFileName();
32
+
33
+        $docs = $reflection->getDocComment();
34
+
35
+        if ($docs !== false) {
36
+            // extract everything prefixed by @ and first letter uppercase
37
+            preg_match_all('/^\h+\*\h+@(?P<annotation>[A-Z]\w+)((?P<parameter>.*))?$/m', $docs, $matches);
38
+            foreach ($matches['annotation'] as $key => $annotation) {
39
+                $annotation = strtolower($annotation);
40
+                $annotationValue = $matches['parameter'][$key];
41
+                if (str_starts_with($annotationValue, '(') && str_ends_with($annotationValue, ')')) {
42
+                    $cutString = substr($annotationValue, 1, -1);
43
+                    $cutString = str_replace(' ', '', $cutString);
44
+                    $splitArray = explode(',', $cutString);
45
+                    foreach ($splitArray as $annotationValues) {
46
+                        [$key, $value] = explode('=', $annotationValues);
47
+                        $this->annotations[$annotation][$key] = $value;
48
+                    }
49
+                    continue;
50
+                }
51
+
52
+                $this->annotations[$annotation] = [$annotationValue];
53
+            }
54
+
55
+            // extract type parameter information
56
+            preg_match_all('/@param\h+(?P<type>\w+)\h+\$(?P<var>\w+)/', $docs, $matches);
57
+            $this->types = array_combine($matches['var'], $matches['type']);
58
+            preg_match_all('/@psalm-param\h+(\?)?(?P<type>\w+)<(?P<rangeMin>(-?\d+|min)),\h*(?P<rangeMax>(-?\d+|max))>(\|null)?\h+\$(?P<var>\w+)/', $docs, $matches);
59
+            foreach ($matches['var'] as $index => $varName) {
60
+                if ($matches['type'][$index] !== 'int') {
61
+                    // only int ranges are possible at the moment
62
+                    // @see https://psalm.dev/docs/annotating_code/type_syntax/scalar_types
63
+                    continue;
64
+                }
65
+                $this->ranges[$varName] = [
66
+                    'min' => $matches['rangeMin'][$index] === 'min' ? PHP_INT_MIN : (int)$matches['rangeMin'][$index],
67
+                    'max' => $matches['rangeMax'][$index] === 'max' ? PHP_INT_MAX : (int)$matches['rangeMax'][$index],
68
+                ];
69
+            }
70
+        }
71
+
72
+        foreach ($reflection->getParameters() as $param) {
73
+            // extract type information from PHP 7 scalar types and prefer them over phpdoc annotations
74
+            $type = $param->getType();
75
+            if ($type instanceof \ReflectionNamedType) {
76
+                $this->types[$param->getName()] = $type->getName();
77
+            }
78
+
79
+            $default = null;
80
+            if ($param->isOptional()) {
81
+                $default = $param->getDefaultValue();
82
+            }
83
+            $this->parameters[$param->name] = $default;
84
+        }
85
+    }
86
+
87
+    /**
88
+     * Inspects the PHPDoc parameters for types
89
+     * @param string $parameter the parameter whose type comments should be
90
+     *                          parsed
91
+     * @return string|null type in the type parameters (@param int $something)
92
+     *                     would return int or null if not existing
93
+     */
94
+    public function getType(string $parameter) {
95
+        if (array_key_exists($parameter, $this->types)) {
96
+            return $this->types[$parameter];
97
+        }
98
+
99
+        return null;
100
+    }
101
+
102
+    public function getRange(string $parameter): ?array {
103
+        if (array_key_exists($parameter, $this->ranges)) {
104
+            return $this->ranges[$parameter];
105
+        }
106
+
107
+        return null;
108
+    }
109
+
110
+    /**
111
+     * @return array the arguments of the method with key => default value
112
+     */
113
+    public function getParameters(): array {
114
+        return $this->parameters;
115
+    }
116
+
117
+    /**
118
+     * Check if a method contains an annotation
119
+     * @param string $name the name of the annotation
120
+     * @return bool true if the annotation is found
121
+     */
122
+    public function hasAnnotation(string $name): bool {
123
+        $name = strtolower($name);
124
+        return array_key_exists($name, $this->annotations);
125
+    }
126
+
127
+    /**
128
+     * Get optional annotation parameter by key
129
+     *
130
+     * @param string $name the name of the annotation
131
+     * @param string $key the string of the annotation
132
+     * @return string
133
+     */
134
+    public function getAnnotationParameter(string $name, string $key): string {
135
+        $name = strtolower($name);
136
+        if (isset($this->annotations[$name][$key])) {
137
+            return $this->annotations[$name][$key];
138
+        }
139
+
140
+        return '';
141
+    }
142
+
143
+    public function getStartLine(): int {
144
+        return $this->startLine;
145
+    }
146
+
147
+    public function getFile(): string {
148
+        return $this->file;
149
+    }
150 150
 }
Please login to merge, or discard this patch.