diff --git a/.composer.json b/.composer.json index bf51e7a4cf..fede7a4122 100644 --- a/.composer.json +++ b/.composer.json @@ -8,8 +8,9 @@ "../../flow doctrine:migrate --quiet", "../../bin/behat -f progress -c Neos.Flow/Tests/Behavior/behat.yml" ], + "lint:phpstan": "../../bin/phpstan analyse", "lint": [ - "../../bin/phpstan analyse" + "@lint:phpstan" ] }, "require": { diff --git a/Neos.Cache/Classes/Frontend/FrontendInterface.php b/Neos.Cache/Classes/Frontend/FrontendInterface.php index 2b4ac9408b..8de9127f6c 100644 --- a/Neos.Cache/Classes/Frontend/FrontendInterface.php +++ b/Neos.Cache/Classes/Frontend/FrontendInterface.php @@ -62,7 +62,7 @@ public function set(string $entryIdentifier, $data, array $tags = [], int $lifet * Finds and returns data from the cache. * * @param string $entryIdentifier Something which identifies the cache entry - depends on concrete cache - * @return mixed + * @return mixed The value or false if the cache entry could not be loaded * @api */ public function get(string $entryIdentifier); diff --git a/Neos.Cache/Classes/Frontend/StringFrontend.php b/Neos.Cache/Classes/Frontend/StringFrontend.php index 1663f91eba..6d0a33568d 100644 --- a/Neos.Cache/Classes/Frontend/StringFrontend.php +++ b/Neos.Cache/Classes/Frontend/StringFrontend.php @@ -58,7 +58,7 @@ public function set(string $entryIdentifier, $string, array $tags = [], int $lif * Finds and returns a variable value from the cache. * * @param string $entryIdentifier Identifier of the cache entry to fetch - * @return string The value + * @return string|false The value or false if the cache entry could not be loaded * @throws \InvalidArgumentException * @api */ diff --git a/Neos.Flow/Classes/Cache/CacheFactory.php b/Neos.Flow/Classes/Cache/CacheFactory.php index 123bd70731..1b2f885775 100644 --- a/Neos.Flow/Classes/Cache/CacheFactory.php +++ b/Neos.Flow/Classes/Cache/CacheFactory.php @@ -38,13 +38,6 @@ class CacheFactory extends \Neos\Cache\CacheFactory */ protected $context; - /** - * A reference to the cache manager - * - * @var CacheManager - */ - protected $cacheManager; - /** * @var Environment */ @@ -55,16 +48,6 @@ class CacheFactory extends \Neos\Cache\CacheFactory */ protected $environmentConfiguration; - /** - * @param CacheManager $cacheManager - * - * @Flow\Autowiring (enabled=false) - */ - public function injectCacheManager(CacheManager $cacheManager): void - { - $this->cacheManager = $cacheManager; - } - /** * @param EnvironmentConfiguration $environmentConfiguration * diff --git a/Neos.Flow/Classes/Cache/CacheManager.php b/Neos.Flow/Classes/Cache/CacheManager.php index 1ca19ca382..ede879875d 100644 --- a/Neos.Flow/Classes/Cache/CacheManager.php +++ b/Neos.Flow/Classes/Cache/CacheManager.php @@ -11,6 +11,7 @@ * source code. */ +use Neos\Cache\CacheFactoryInterface; use Neos\Flow\Annotations as Flow; use Neos\Cache\Backend\FileBackend; use Neos\Cache\Exception\DuplicateIdentifierException; @@ -37,7 +38,7 @@ class CacheManager { /** - * @var CacheFactory + * @var CacheFactoryInterface */ protected $cacheFactory; @@ -100,10 +101,10 @@ public function injectLogger(LoggerInterface $logger) } /** - * @param CacheFactory $cacheFactory + * @param CacheFactoryInterface $cacheFactory * @return void */ - public function injectCacheFactory(CacheFactory $cacheFactory): void + public function injectCacheFactory(CacheFactoryInterface $cacheFactory): void { $this->cacheFactory = $cacheFactory; } @@ -496,6 +497,7 @@ protected function createCache(string $identifier): void $backend = isset($this->cacheConfigurations[$identifier]['backend']) ? $this->cacheConfigurations[$identifier]['backend'] : $this->cacheConfigurations['Default']['backend']; $backendOptions = isset($this->cacheConfigurations[$identifier]['backendOptions']) ? $this->cacheConfigurations[$identifier]['backendOptions'] : $this->cacheConfigurations['Default']['backendOptions']; $persistent = isset($this->cacheConfigurations[$identifier]['persistent']) ? $this->cacheConfigurations[$identifier]['persistent'] : $this->cacheConfigurations['Default']['persistent']; + // @phpstan-ignore-next-line - $persistent is not yet part of the CacheFactoryInterface $cache = $this->cacheFactory->create($identifier, $frontend, $backend, $backendOptions, $persistent); $this->registerCache($cache, $persistent); } diff --git a/Neos.Flow/Classes/Command/RoutingCommandController.php b/Neos.Flow/Classes/Command/RoutingCommandController.php index acb33fe95c..ae43d4907a 100644 --- a/Neos.Flow/Classes/Command/RoutingCommandController.php +++ b/Neos.Flow/Classes/Command/RoutingCommandController.php @@ -17,7 +17,6 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; use Neos\Flow\Cli\Exception\StopCommandException; -use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Http\Helper\RequestInformationHelper; use Neos\Flow\Mvc\Exception\InvalidRoutePartValueException; use Neos\Flow\Mvc\Routing\Dto\ResolveContext; @@ -26,7 +25,7 @@ use Neos\Flow\Mvc\Routing\Dto\RouteTags; use Neos\Flow\Mvc\Routing\Dto\UriConstraints; use Neos\Flow\Mvc\Routing\Route; -use Neos\Flow\Mvc\Routing\Router; +use Neos\Flow\Mvc\Routing\RoutesProviderInterface; use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Http\Factories\ServerRequestFactory; use Neos\Utility\Arrays; @@ -40,15 +39,9 @@ class RoutingCommandController extends CommandController { /** * @Flow\Inject - * @var ConfigurationManager + * @var RoutesProviderInterface */ - protected $configurationManager; - - /** - * @Flow\Inject - * @var Router - */ - protected $router; + protected $routesProvider; /** * @Flow\Inject @@ -73,8 +66,7 @@ public function listCommand(): void { $this->outputLine('Currently registered routes:'); $rows = []; - /** @var Route $route */ - foreach ($this->router->getRoutes() as $index => $route) { + foreach ($this->routesProvider->getRoutes() as $index => $route) { $routeNumber = $index + 1; $rows[] = [ '#' => $routeNumber, @@ -99,15 +91,12 @@ public function listCommand(): void */ public function showCommand(int $index): void { - /** @var Route[] $routes */ - $routes = $this->router->getRoutes(); - if (!isset($routes[$index - 1])) { + $route = $this->routesProvider->getRoutes()[$index - 1] ?? null; + if ($route === null) { $this->outputLine('Route %d was not found!', [$index]); $this->outputLine('Run ./flow routing:list to show all registered routes'); $this->quit(1); - return; } - $route = $routes[$index - 1]; $this->outputLine('Information for route #' . $index . ':'); $this->outputLine(); @@ -203,8 +192,8 @@ public function resolveCommand(string $package, string $controller = null, strin /** @var Route|null $resolvedRoute */ $resolvedRoute = null; $resolvedRouteNumber = 0; - /** @var int $index */ - foreach ($this->router->getRoutes() as $index => $route) { + + foreach ($this->routesProvider->getRoutes() as $index => $route) { /** @var Route $route */ if ($route->resolves($resolveContext) === true) { $resolvedRoute = $route; @@ -216,7 +205,6 @@ public function resolveCommand(string $package, string $controller = null, strin if ($resolvedRoute === null) { $this->outputLine('No route could resolve these values...'); $this->quit(1); - return; } /** @var UriConstraints $uriConstraints */ @@ -273,7 +261,6 @@ public function matchCommand(string $uri, string $method = null, string $paramet if (isset($requestUri->getPath()[0]) && $requestUri->getPath()[0] !== '/') { $this->outputLine('The URI "%s" is not valid. The path has to start with a "/"', [$requestUri]); $this->quit(1); - return; } $httpRequest = $this->serverRequestFactory->createServerRequest($method, $requestUri); $routeParameters = $this->createRouteParametersFromJson($parameters); @@ -292,8 +279,7 @@ public function matchCommand(string $uri, string $method = null, string $paramet /** @var Route|null $matchedRoute */ $matchedRoute = null; $matchedRouteNumber = 0; - /** @var int $index */ - foreach ($this->router->getRoutes() as $index => $route) { + foreach ($this->routesProvider->getRoutes() as $index => $route) { /** @var Route $route */ if ($route->matches($routeContext) === true) { $matchedRoute = $route; @@ -305,7 +291,6 @@ public function matchCommand(string $uri, string $method = null, string $paramet if ($matchedRoute === null) { $this->outputLine('No route could match %s request to URL %s...', [$method, $requestUri]); $this->quit(1); - return; } $this->outputLine('Route matched!'); @@ -365,12 +350,10 @@ private function parseJsonToArray(?string $json): array if ($parsedValue === null && \json_last_error() !== JSON_ERROR_NONE) { $this->outputLine('Failed to parse %s as JSON: %s', [$json, \json_last_error_msg()]); $this->quit(1); - return []; } if (!is_array($parsedValue)) { $this->outputLine('Failed to parse %s to an array, please a provide valid JSON object that can be represented as PHP array', [$json]); $this->quit(1); - return []; } return $parsedValue; } diff --git a/Neos.Flow/Classes/Core/Booting/Scripts.php b/Neos.Flow/Classes/Core/Booting/Scripts.php index ae7b28cad9..d38993b1b1 100644 --- a/Neos.Flow/Classes/Core/Booting/Scripts.php +++ b/Neos.Flow/Classes/Core/Booting/Scripts.php @@ -356,10 +356,22 @@ public static function initializeCacheManagement(Bootstrap $bootstrap) $configurationManager = $bootstrap->getEarlyInstance(ConfigurationManager::class); $environment = $bootstrap->getEarlyInstance(Environment::class); - $cacheFactoryObjectConfiguration = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_OBJECTS, CacheFactoryInterface::class); - $cacheFactoryClass = isset($cacheFactoryObjectConfiguration['className']) ? $cacheFactoryObjectConfiguration['className'] : CacheFactory::class; + /** + * Workaround to find the correct CacheFactory implementation at compile time. + * We can rely on the $objectConfiguration being ordered by the package names after their loading order. + * The object manager _does_ even know that at a later step in compile time: {@see CompileTimeObjectManager::getClassNameByObjectName()} + * But at this time it is not available. https://github.com/neos/flow-development-collection/issues/3317 + */ + $cacheFactoryClass = CacheFactory::class; + $cacheFactoryObjectConfiguration = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_OBJECTS); + foreach ($cacheFactoryObjectConfiguration as $objectConfiguration) { + if (isset($objectConfiguration[CacheFactoryInterface::class]['className'])) { + // use the implementation of the package with the highest loading order + $cacheFactoryClass = $objectConfiguration[CacheFactoryInterface::class]['className']; + } + } - /** @var CacheFactory $cacheFactory */ + /** @var CacheFactoryInterface $cacheFactory */ $cacheFactory = new $cacheFactoryClass($bootstrap->getContext(), $environment, $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Neos.Flow.cache.applicationIdentifier')); $cacheManager = new CacheManager(); @@ -369,8 +381,6 @@ public static function initializeCacheManagement(Bootstrap $bootstrap) $cacheManager->injectEnvironment($environment); $cacheManager->injectCacheFactory($cacheFactory); - $cacheFactory->injectCacheManager($cacheManager); - $bootstrap->setEarlyInstance(CacheManager::class, $cacheManager); $bootstrap->setEarlyInstance(CacheFactory::class, $cacheFactory); } @@ -861,6 +871,7 @@ protected static function ensureCLISubrequestsUseCurrentlyRunningPhpBinary($phpB $command[] = '2>&1'; // Output errors in response // Try to resolve which binary file PHP is pointing to + $output = []; exec(join(' ', $command), $output, $result); if ($result === 0 && count($output) === 1) { @@ -883,6 +894,7 @@ protected static function ensureCLISubrequestsUseCurrentlyRunningPhpBinary($phpB $realPhpBinary = @realpath(PHP_BINARY); if ($realPhpBinary === false) { // bypass with exec open_basedir restriction + $output = []; exec(PHP_BINARY . ' -r "echo realpath(PHP_BINARY);"', $output); $realPhpBinary = $output[0]; } diff --git a/Neos.Flow/Classes/Error/DebugExceptionHandler.php b/Neos.Flow/Classes/Error/DebugExceptionHandler.php index 5c64ab867c..f64b7b4f0a 100644 --- a/Neos.Flow/Classes/Error/DebugExceptionHandler.php +++ b/Neos.Flow/Classes/Error/DebugExceptionHandler.php @@ -96,11 +96,17 @@ protected function renderStatically(int $statusCode, \Throwable $exception) while (true) { $filepaths = Debugger::findProxyAndShortFilePath($exception->getFile()); $filePathAndName = $filepaths['proxy'] !== '' ? $filepaths['proxy'] : $filepaths['short']; - $exceptionMessageParts = $this->splitExceptionMessage($exception->getMessage()); - $exceptionHeader .= '

' . htmlspecialchars($exceptionMessageParts['subject']) . '

'; - if ($exceptionMessageParts['body'] !== '') { - $exceptionHeader .= '

' . nl2br(htmlspecialchars($exceptionMessageParts['body'])) . '

'; + ['subject' => $exceptionMessageSubject, 'body' => $exceptionMessageBody] = $this->splitExceptionMessage($exception->getMessage()); + + $exceptionHeader .= '

' . htmlspecialchars($exceptionMessageSubject) . '

'; + if ($exceptionMessageBody !== '') { + if (str_contains($exceptionMessageBody, ' ')) { + // contents with multiple spaces will be pre-served + $exceptionHeader .= '

' . htmlspecialchars($exceptionMessageBody) . '

'; + } else { + $exceptionHeader .= '

' . nl2br(htmlspecialchars($exceptionMessageBody)) . '

'; + } } $exceptionHeader .= ''; diff --git a/Neos.Flow/Classes/Error/Debugger.php b/Neos.Flow/Classes/Error/Debugger.php index 235bbc92b7..cd53b7c1cf 100644 --- a/Neos.Flow/Classes/Error/Debugger.php +++ b/Neos.Flow/Classes/Error/Debugger.php @@ -286,6 +286,9 @@ protected static function renderObjectDump($object, int $level, bool $renderProp if (preg_match(self::$excludedPropertyNames, $property->getName())) { continue; } + if ($property->isStatic()) { + continue; + } $dump .= chr(10); $dump .= str_repeat(' ', $level) . ($plaintext ? '' : '') . self::ansiEscapeWrap($property->getName(), '36', $ansiColors) . ($plaintext ? '' : '') . ' => '; $property->setAccessible(true); diff --git a/Neos.Flow/Classes/Mvc/ActionResponse.php b/Neos.Flow/Classes/Mvc/ActionResponse.php index 8671eda844..da68121c92 100644 --- a/Neos.Flow/Classes/Mvc/ActionResponse.php +++ b/Neos.Flow/Classes/Mvc/ActionResponse.php @@ -3,6 +3,8 @@ use GuzzleHttp\Psr7\Utils; use Neos\Flow\Http\Cookie; +use Neos\Flow\Mvc\Controller\AbstractController; +use Neos\Flow\Mvc\Controller\ControllerContext; use Psr\Http\Message\ResponseInterface; use Neos\Flow\Annotations as Flow; use Psr\Http\Message\StreamInterface; @@ -10,11 +12,34 @@ use GuzzleHttp\Psr7\Response; /** - * The minimal MVC response object. - * It allows for simple interactions with the HTTP response from within MVC actions. More specific requirements can be implemented via HTTP middlewares. + * The legacy MVC response object. * + * Previously Flows MVC needed a single mutable response which was passed from dispatcher to controllers + * and even further to the view and other places via the controller context: {@see ControllerContext::getResponse()}. + * This allowed to manipulate the response at every place. + * + * With the dispatcher and controllers now directly returning a response, the mutability is no longer required. + * Additionally, the abstraction offers naturally nothing, that cant be archived by the psr response, + * as it directly translates to one: {@see ActionResponse::buildHttpResponse()} + * + * So you can and should use the immutable psr {@see ResponseInterface} instead where-ever possible. + * + * For backwards compatibility, each controller will might now manage an own instance of the action response + * via `$this->response` {@see AbstractController::$response} and pass it along to places. + * But this behaviour is deprecated! + * + * Instead, you can directly return a PSR repose {@see \GuzzleHttp\Psr7\Response} from a controller: + * + * ```php + * public function myAction() + * { + * return (new Response(status: 200, body: $output)) + * ->withAddedHeader('X-My-Header', 'foo'); + * } + * ``` + * + * @deprecated with Flow 9 * @Flow\Proxy(false) - * @api */ final class ActionResponse { @@ -66,7 +91,7 @@ public function __construct() /** * @param string|StreamInterface $content * @return void - * @api + * @deprecated please use {@see ResponseInterface::withBody()} in combination with {@see \GuzzleHttp\Psr7\Utils::streamFor} instead */ public function setContent($content): void { @@ -82,7 +107,7 @@ public function setContent($content): void * * @param string $contentType * @return void - * @api + * @deprecated please use {@see ResponseInterface::withHeader()} with "Content-Type" instead. */ public function setContentType(string $contentType): void { @@ -95,7 +120,7 @@ public function setContentType(string $contentType): void * @param UriInterface $uri * @param int $statusCode * @return void - * @api + * @deprecated please use {@see ResponseInterface::withStatus()} and {@see ResponseInterface::withHeader()} with "Header" instead. */ public function setRedirectUri(UriInterface $uri, int $statusCode = 303): void { @@ -109,7 +134,7 @@ public function setRedirectUri(UriInterface $uri, int $statusCode = 303): void * * @param int $statusCode * @return void - * @api + * @deprecated please use {@see ResponseInterface::withStatus()} instead. */ public function setStatusCode(int $statusCode): void { @@ -121,7 +146,7 @@ public function setStatusCode(int $statusCode): void * This leads to a corresponding `Set-Cookie` header to be set in the HTTP response * * @param Cookie $cookie Cookie to be set in the HTTP response - * @api + * @deprecated please use {@see ResponseInterface::withHeader()} with "Set-Cookie" instead. */ public function setCookie(Cookie $cookie): void { @@ -133,7 +158,7 @@ public function setCookie(Cookie $cookie): void * This leads to a corresponding `Set-Cookie` header with an expired Cookie to be set in the HTTP response * * @param string $cookieName Name of the cookie to delete - * @api + * @deprecated */ public function deleteCookie(string $cookieName): void { @@ -145,6 +170,8 @@ public function deleteCookie(string $cookieName): void /** * Set the specified header in the response, overwriting any previous value set for this header. * + * This behaviour is unsafe and partially unspecified: https://github.com/neos/flow-development-collection/issues/2492 + * * @param string $headerName The name of the header to set * @param array|string|\DateTime $headerValue An array of values or a single value for the specified header field * @return void @@ -163,6 +190,8 @@ public function setHttpHeader(string $headerName, $headerValue): void /** * Add the specified header to the response, without overwriting any previous value set for this header. * + * This behaviour is unsafe and partially unspecified: https://github.com/neos/flow-development-collection/issues/2492 + * * @param string $headerName The name of the header to set * @param array|string|\DateTime $headerValue An array of values or a single value for the specified header field * @return void @@ -232,7 +261,9 @@ public function getContentType(): string } /** - * Use this if you want build your own HTTP Response inside your action + * Unsafe. Please avoid the use of this escape hatch as the behaviour is partly unspecified + * https://github.com/neos/flow-development-collection/issues/2492 + * * @param ResponseInterface $response */ public function replaceHttpResponse(ResponseInterface $response): void @@ -274,13 +305,16 @@ public function mergeIntoParentResponse(ActionResponse $actionResponse): ActionR } /** - * Note this is a special use case method that will apply the internal properties (Content-Type, StatusCode, Location, Set-Cookie and Content) - * to the given PSR-7 Response and return a modified response. This is used to merge the ActionResponse properties into a possible HttpResponse - * created in a View (see ActionController::renderView()) because those would be overwritten otherwise. Note that any component parameters will - * still run through the component chain and will not be propagated here. + * During the migration of {@see ActionResponse} to {@see HttpResponse} this might come in handy. + * + * Note this is a special use case method that will apply the internal properties + * (Content-Type, StatusCode, Location, Set-Cookie and Content) + * to a new or replaced PSR-7 Response and return it. + * + * Possibly unsafe when used in combination with {@see self::replaceHttpResponse()} + * https://github.com/neos/flow-development-collection/issues/2492 * * @return ResponseInterface - * @internal */ public function buildHttpResponse(): ResponseInterface { diff --git a/Neos.Flow/Classes/Mvc/Controller/AbstractController.php b/Neos.Flow/Classes/Mvc/Controller/AbstractController.php index c36e276391..70d2a6741e 100644 --- a/Neos.Flow/Classes/Mvc/Controller/AbstractController.php +++ b/Neos.Flow/Classes/Mvc/Controller/AbstractController.php @@ -11,6 +11,7 @@ * source code. */ +use GuzzleHttp\Psr7\Utils; use Neos\Error\Messages as Error; use Neos\Flow\Annotations as Flow; use GuzzleHttp\Psr7\Uri; @@ -61,9 +62,9 @@ abstract class AbstractController implements ControllerInterface protected $request; /** - * The response which will be returned by this action controller + * The legacy response which will is provide by this action controller * @var ActionResponse - * @api + * @deprecated with Flow 9 {@see ActionResponse} */ protected $response; @@ -110,15 +111,17 @@ abstract class AbstractController implements ControllerInterface */ protected function initializeController(ActionRequest $request, ActionResponse $response) { + // make the current request and response "globally" available to everywhere in this controller. $this->request = $request; - $this->request->setDispatched(true); $this->response = $response; + $this->request->setDispatched(true); + $this->uriBuilder = new UriBuilder(); - $this->uriBuilder->setRequest($this->request); + $this->uriBuilder->setRequest($request); $this->arguments = new Arguments([]); - $this->controllerContext = new ControllerContext($this->request, $this->response, $this->arguments, $this->uriBuilder); + $this->controllerContext = new ControllerContext($request, $response, $this->arguments, $this->uriBuilder); $mediaType = MediaTypeHelper::negotiateMediaType(MediaTypeHelper::determineAcceptedMediaTypes($request->getHttpRequest()), $this->supportedMediaTypes); if ($mediaType === null) { @@ -126,7 +129,7 @@ protected function initializeController(ActionRequest $request, ActionResponse $ } $this->negotiatedMediaType = $mediaType; if ($request->getFormat() === '') { - $this->request->setFormat(MediaTypes::getFilenameExtensionFromMediaType($mediaType)); + $request->setFormat(MediaTypes::getFilenameExtensionFromMediaType($mediaType)); } } @@ -257,7 +260,7 @@ protected function forwardToRequest(ActionRequest $request): never * @param string $actionName Name of the action to forward to * @param string|null $controllerName Unqualified object name of the controller to forward to. If not specified, the current controller is used. * @param string|null $packageKey Key of the package containing the controller to forward to. If not specified, the current package is assumed. - * @param array $arguments Array of arguments for the target action + * @param array $arguments Array of arguments for the target action * @param integer $delay (optional) The delay in seconds. Default is no delay. * @param integer $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other" * @param string|null $format The format to use for the redirect URI @@ -322,16 +325,21 @@ protected function redirectToRequest(ActionRequest $request, int $delay = 0, int */ protected function redirectToUri(string|UriInterface $uri, int $delay = 0, int $statusCode = 303): never { + $httpResponse = $this->response->buildHttpResponse(); if ($delay === 0) { if (!$uri instanceof UriInterface) { $uri = new Uri($uri); } - $this->response->setRedirectUri($uri, $statusCode); + $httpResponse = $httpResponse + ->withStatus($statusCode) + ->withHeader('Location', (string)$uri); } else { - $this->response->setStatusCode($statusCode); - $this->response->setContent(''); + $content = ''; + $httpResponse = $httpResponse + ->withStatus($statusCode) + ->withBody(Utils::streamFor($content)); } - throw StopActionException::createForResponse($this->response, ''); + throw StopActionException::createForResponse($httpResponse, ''); } /** @@ -347,7 +355,8 @@ protected function redirectToUri(string|UriInterface $uri, int $delay = 0, int $ */ protected function throwStatus(int $statusCode, $statusMessage = null, $content = null): never { - $this->response->setStatusCode($statusCode); + $httpResponse = $this->response->buildHttpResponse(); + $httpResponse = $httpResponse->withStatus($statusCode); if ($content === null) { $content = sprintf( '%s %s', @@ -355,8 +364,8 @@ protected function throwStatus(int $statusCode, $statusMessage = null, $content $statusMessage ?? ResponseInformationHelper::getStatusMessageByCode($statusCode) ); } - $this->response->setContent($content); - throw StopActionException::createForResponse($this->response, $content); + $httpResponse = $httpResponse->withBody(Utils::streamFor($content)); + throw StopActionException::createForResponse($httpResponse, $content); } /** @@ -370,10 +379,10 @@ protected function throwStatus(int $statusCode, $statusMessage = null, $content * @throws \Neos\Flow\Security\Exception * @api */ - protected function mapRequestArgumentsToControllerArguments(ActionRequest $request) + protected function mapRequestArgumentsToControllerArguments(ActionRequest $request, Arguments $arguments) { /* @var $argument \Neos\Flow\Mvc\Controller\Argument */ - foreach ($this->arguments as $argument) { + foreach ($arguments as $argument) { $argumentName = $argument->getName(); if ($argument->getMapRequestBody()) { $argument->setValue($request->getHttpRequest()->getParsedBody()); diff --git a/Neos.Flow/Classes/Mvc/Controller/ActionController.php b/Neos.Flow/Classes/Mvc/Controller/ActionController.php index 4e16b02bcc..1f4074b789 100644 --- a/Neos.Flow/Classes/Mvc/Controller/ActionController.php +++ b/Neos.Flow/Classes/Mvc/Controller/ActionController.php @@ -11,6 +11,7 @@ * source code. */ +use GuzzleHttp\Psr7\Utils; use Neos\Error\Messages\Result; use Neos\Flow\Annotations as Flow; use Neos\Error\Messages as Error; @@ -33,8 +34,6 @@ use Neos\Flow\Property\Exception\TargetNotFoundException; use Neos\Flow\Property\TypeConverter\Error\TargetNotFoundError; use Neos\Flow\Reflection\ReflectionService; -use Neos\Flow\Security\Exception\InvalidArgumentForHashGenerationException; -use Neos\Flow\Security\Exception\InvalidHashException; use Neos\Utility\TypeHandling; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; @@ -203,7 +202,7 @@ public function injectThrowableStorage(ThrowableStorageInterface $throwableStora * Handles a request. The result output is returned by altering the given response. * * @param ActionRequest $request The request object - * @return ActionResponse + * @return ResponseInterface * @throws InvalidActionVisibilityException * @throws InvalidArgumentTypeException * @throws NoSuchActionException @@ -215,16 +214,16 @@ public function injectThrowableStorage(ThrowableStorageInterface $throwableStora * @throws \Neos\Flow\Security\Exception * @api */ - public function processRequest(ActionRequest $request): ActionResponse + public function processRequest(ActionRequest $request): ResponseInterface { $response = new ActionResponse(); $this->initializeController($request, $response); $this->actionMethodName = $this->resolveActionMethodName($request); - $this->initializeActionMethodArguments(); + $this->initializeActionMethodArguments($this->arguments); if ($this->enableDynamicTypeValidation !== true) { - $this->initializeActionMethodValidators(); + $this->initializeActionMethodValidators($this->arguments); } $this->initializeAction(); @@ -232,23 +231,18 @@ public function processRequest(ActionRequest $request): ActionResponse if (method_exists($this, $actionInitializationMethodName)) { call_user_func([$this, $actionInitializationMethodName]); } - try { - $this->mvcPropertyMappingConfigurationService->initializePropertyMappingConfigurationFromRequest($request, $this->arguments); - } catch (InvalidArgumentForHashGenerationException|InvalidHashException $e) { - $message = $this->throwableStorage->logThrowable($e); - $this->logger->notice('Property mapping configuration failed due to HMAC errors. ' . $message, LogEnvironment::fromMethodName(__METHOD__)); - $this->throwStatus(400, null, 'Invalid HMAC submitted'); - } + + $this->mvcPropertyMappingConfigurationService->initializePropertyMappingConfigurationFromRequest($request, $this->arguments); try { - $this->mapRequestArgumentsToControllerArguments($request); + $this->mapRequestArgumentsToControllerArguments($request, $this->arguments); } catch (RequiredArgumentMissingException $e) { $message = $this->throwableStorage->logThrowable($e); $this->logger->notice('Request argument mapping failed due to a missing required argument. ' . $message, LogEnvironment::fromMethodName(__METHOD__)); $this->throwStatus(400, null, 'Required argument is missing'); } if ($this->enableDynamicTypeValidation === true) { - $this->initializeActionMethodValidators(); + $this->initializeActionMethodValidators($this->arguments); } if ($this->view === null) { @@ -260,14 +254,13 @@ public function processRequest(ActionRequest $request): ActionResponse $this->initializeView($this->view); } - // We still use a global response here as it might have been changed in any of the steps above - $response = $this->callActionMethod($request, $this->response); + $httpResponse = $this->callActionMethod($request, $this->arguments, $response->buildHttpResponse()); - if (!$response->hasContentType()) { - $response->setContentType($this->negotiatedMediaType); + if (!$httpResponse->hasHeader('Content-Type')) { + $httpResponse = $httpResponse->withHeader('Content-Type', $this->negotiatedMediaType); } - return $response; + return $httpResponse; } /** @@ -301,7 +294,7 @@ protected function resolveActionMethodName(ActionRequest $request): string * @throws InvalidArgumentTypeException * @see initializeArguments() */ - protected function initializeActionMethodArguments() + protected function initializeActionMethodArguments(Arguments $arguments) { $actionMethodParameters = static::getActionMethodParameters($this->objectManager); if (isset($actionMethodParameters[$this->actionMethodName])) { @@ -310,7 +303,7 @@ protected function initializeActionMethodArguments() $methodParameters = []; } - $this->arguments->removeAll(); + $arguments->removeAll(); foreach ($methodParameters as $parameterName => $parameterInfo) { $dataType = null; if (isset($parameterInfo['type'])) { @@ -326,7 +319,7 @@ protected function initializeActionMethodArguments() $dataType = TypeHandling::stripNullableType($dataType); } $mapRequestBody = isset($parameterInfo['mapRequestBody']) && $parameterInfo['mapRequestBody'] === true; - $this->arguments->addNewArgument($parameterName, $dataType, ($parameterInfo['optional'] === false), $defaultValue, $mapRequestBody); + $arguments->addNewArgument($parameterName, $dataType, ($parameterInfo['optional'] === false), $defaultValue, $mapRequestBody); } } @@ -390,7 +383,7 @@ protected function getInformationNeededForInitializeActionMethodValidators() * * @return void */ - protected function initializeActionMethodValidators() + protected function initializeActionMethodValidators(Arguments $arguments) { [$validateGroupAnnotations, $actionMethodParameters, $actionValidateAnnotations, $actionIgnoredArguments] = $this->getInformationNeededForInitializeActionMethodValidators(); @@ -420,7 +413,7 @@ protected function initializeActionMethodValidators() } /* @var $argument Argument */ - foreach ($this->arguments as $argument) { + foreach ($arguments as $argument) { $argumentName = $argument->getName(); if (isset($ignoredArguments[$argumentName]) && !$ignoredArguments[$argumentName]['evaluate']) { continue; @@ -515,17 +508,17 @@ protected function initializeAction() * view exists, the view is rendered automatically. * * @param ActionRequest $request - * @param ActionResponse $response - * @return ActionResponse + * @param Arguments $arguments + * @param ResponseInterface $httpResponse The most likely empty response, previously available as $this->response */ - protected function callActionMethod(ActionRequest $request, ActionResponse $response): ActionResponse + protected function callActionMethod(ActionRequest $request, Arguments $arguments, ResponseInterface $httpResponse): ResponseInterface { $preparedArguments = []; - foreach ($this->arguments as $argument) { + foreach ($arguments as $argument) { $preparedArguments[] = $argument->getValue(); } - $validationResult = $this->arguments->getValidationResults(); + $validationResult = $arguments->getValidationResults(); if (!$validationResult->hasErrors()) { $actionResult = $this->{$this->actionMethodName}(...$preparedArguments); @@ -559,13 +552,15 @@ protected function callActionMethod(ActionRequest $request, ActionResponse $resp } } + if ($actionResult instanceof ResponseInterface) { + return $actionResult; + } + if ($actionResult === null && $this->view instanceof ViewInterface) { - $this->response = $this->renderView($this->response); - } else { - $this->response->setContent($actionResult); + return $this->renderView($httpResponse); } - return $this->response; + return $httpResponse->withBody(Utils::streamFor($actionResult)); } /** @@ -831,36 +826,48 @@ protected function getErrorFlashMessage() /** * Renders the view and applies the result to the response object. * - * @param ActionResponse $response - * @return ActionResponse + * @param ResponseInterface $httpResponse The most likely empty response, previously available as $this->response */ - protected function renderView(ActionResponse $response): ActionResponse + protected function renderView(ResponseInterface $httpResponse): ResponseInterface { $result = $this->view->render(); if (is_string($result)) { - $response->setContent($result); + return $httpResponse->withBody(Utils::streamFor($result)); } if ($result instanceof ActionResponse) { - $result->mergeIntoParentResponse($response); + // deprecated behaviour to return an ActionResponse from a view + $subResponse = $result->buildHttpResponse(); + // legacy behaviour of "mergeIntoParentResponse": + // transfer possible headers + foreach ($subResponse->getHeaders() as $name => $values) { + $httpResponse = $httpResponse->withHeader($name, $values); + } + // if the status code is 200 we assume it's the default and will not overrule it + if ($subResponse->getStatusCode() !== 200) { + $httpResponse = $httpResponse->withStatus($subResponse->getStatusCode()); + } + // if the known body size is not empty replace the body + if ($subResponse->getBody()->getSize() !== 0) { + $httpResponse = $httpResponse->withBody($subResponse->getBody()); + } + return $httpResponse; } if ($result instanceof ResponseInterface) { - $response->replaceHttpResponse($result); - if ($result->hasHeader('Content-Type')) { - $response->setContentType($result->getHeaderLine('Content-Type')); - } + return $result; } - if (is_object($result) && is_callable([$result, '__toString'])) { - $response->setContent((string)$result); + if ($result instanceof StreamInterface) { + return $httpResponse->withBody($result); } - if ($result instanceof StreamInterface) { - $response->setContent($result); + if ($result instanceof \Stringable) { + return $httpResponse->withBody(Utils::streamFor((string)$result)); } - return $response; + // Case should not happen. Contract of the view was not obeyed. + return $httpResponse; } } diff --git a/Neos.Flow/Classes/Mvc/Controller/Arguments.php b/Neos.Flow/Classes/Mvc/Controller/Arguments.php index f1206c7ed6..a5267d9ce9 100644 --- a/Neos.Flow/Classes/Mvc/Controller/Arguments.php +++ b/Neos.Flow/Classes/Mvc/Controller/Arguments.php @@ -78,9 +78,6 @@ public function offsetUnset($offset): void parent::offsetUnset($translatedOffset); unset($this->argumentNames[$translatedOffset]); - if ($offset != $translatedOffset) { - unset($this->argumentShortNames[$offset]); - } } /** diff --git a/Neos.Flow/Classes/Mvc/Controller/ControllerContext.php b/Neos.Flow/Classes/Mvc/Controller/ControllerContext.php index b6ce418831..94d37a825c 100644 --- a/Neos.Flow/Classes/Mvc/Controller/ControllerContext.php +++ b/Neos.Flow/Classes/Mvc/Controller/ControllerContext.php @@ -82,10 +82,10 @@ public function getRequest() } /** - * Get the response of the controller + * The legacy response of the controller. * * @return ActionResponse - * @api + * @deprecated with Flow 9 {@see ActionResponse} */ public function getResponse() { diff --git a/Neos.Flow/Classes/Mvc/Controller/ControllerInterface.php b/Neos.Flow/Classes/Mvc/Controller/ControllerInterface.php index 515db1d117..e6e3220b9a 100644 --- a/Neos.Flow/Classes/Mvc/Controller/ControllerInterface.php +++ b/Neos.Flow/Classes/Mvc/Controller/ControllerInterface.php @@ -12,9 +12,9 @@ */ use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\ActionResponse; use Neos\Flow\Mvc\Exception\ForwardException; use Neos\Flow\Mvc\Exception\StopActionException; +use Psr\Http\Message\ResponseInterface; /** * Generic interface for controllers @@ -43,10 +43,10 @@ interface ControllerInterface * wich the Dispatcher will catch and handle its attached next-request. * * @param ActionRequest $request The dispatched action request - * @return ActionResponse The resulting created response + * @return ResponseInterface The resulting created response * @throws StopActionException * @throws ForwardException * @api */ - public function processRequest(ActionRequest $request): ActionResponse; + public function processRequest(ActionRequest $request): ResponseInterface; } diff --git a/Neos.Flow/Classes/Mvc/Controller/RestController.php b/Neos.Flow/Classes/Mvc/Controller/RestController.php index 79ec113edf..849c637e94 100644 --- a/Neos.Flow/Classes/Mvc/Controller/RestController.php +++ b/Neos.Flow/Classes/Mvc/Controller/RestController.php @@ -11,8 +11,7 @@ * source code. */ -use GuzzleHttp\Psr7\Uri; -use Neos\Flow\Annotations as Flow; +use GuzzleHttp\Psr7\BufferStream; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\ActionResponse; use Neos\Flow\Mvc\Exception\InvalidActionNameException; @@ -128,13 +127,15 @@ protected function initializeUpdateAction() */ protected function redirectToUri(string|UriInterface $uri, int $delay = 0, int $statusCode = 303): never { - // the parent method throws the exception, but we need to act afterwards - // thus the code in catch - it's the expected state + // the parent method throws the exception, we decorate it afterwards try { parent::redirectToUri($uri, $delay, $statusCode); } catch (StopActionException $exception) { if ($this->request->getFormat() === 'json') { - $exception->response->setContent(''); + throw StopActionException::createForResponse( + $exception->response->withBody(new BufferStream()), + 'Intercepted to sent empty body for JSON request.' + ); } throw $exception; } diff --git a/Neos.Flow/Classes/Mvc/DispatchMiddleware.php b/Neos.Flow/Classes/Mvc/DispatchMiddleware.php index 7e1b4c0bc4..318b3702eb 100644 --- a/Neos.Flow/Classes/Mvc/DispatchMiddleware.php +++ b/Neos.Flow/Classes/Mvc/DispatchMiddleware.php @@ -41,6 +41,6 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface throw new Exception('No ActionRequest was created before the DispatchMiddleware. Make sure you have the SecurityEntryPointMiddleware configured before dispatch.', 1605091292); } - return $this->dispatcher->dispatch($actionRequest)->buildHttpResponse(); + return $this->dispatcher->dispatch($actionRequest); } } diff --git a/Neos.Flow/Classes/Mvc/Dispatcher.php b/Neos.Flow/Classes/Mvc/Dispatcher.php index e952c7b0a3..bf9f941b6f 100644 --- a/Neos.Flow/Classes/Mvc/Dispatcher.php +++ b/Neos.Flow/Classes/Mvc/Dispatcher.php @@ -11,6 +11,7 @@ * source code. */ +use GuzzleHttp\Psr7\Response; use Neos\Flow\Annotations as Flow; use Neos\Flow\Configuration\Exception\NoSuchOptionException; use Neos\Flow\Log\PsrLoggerFactoryInterface; @@ -26,6 +27,7 @@ use Neos\Flow\Security\Exception\AccessDeniedException; use Neos\Flow\Security\Exception\AuthenticationRequiredException; use Neos\Flow\Security\Exception\MissingConfigurationException; +use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; /** @@ -84,7 +86,6 @@ public function injectFirewall(FirewallInterface $firewall) * Dispatches a request to a controller * * @param ActionRequest $request The request to dispatch - * @return ActionResponse * @throws AccessDeniedException * @throws AuthenticationRequiredException * @throws InfiniteLoopException @@ -93,7 +94,7 @@ public function injectFirewall(FirewallInterface $firewall) * @throws MissingConfigurationException * @api */ - public function dispatch(ActionRequest $request): ActionResponse + public function dispatch(ActionRequest $request): ResponseInterface { try { if ($this->securityContext->areAuthorizationChecksDisabled() !== true) { @@ -117,10 +118,9 @@ public function dispatch(ActionRequest $request): ActionResponse * Try processing the request until it is successfully marked "dispatched" * * @param ActionRequest $request - * @return ActionResponse * @throws InvalidControllerException|InfiniteLoopException|NoSuchOptionException */ - protected function initiateDispatchLoop(ActionRequest $request): ActionResponse + protected function initiateDispatchLoop(ActionRequest $request): ResponseInterface { $dispatchLoopCount = 0; while ($request->isDispatched() === false) { @@ -144,7 +144,7 @@ protected function initiateDispatchLoop(ActionRequest $request): ActionResponse } } // TODO $response is never _null_ at this point, except a `forwardToRequest` and the `nextRequest` is already dispatched == true, which seems illegal af - return $response ?? new ActionResponse(); + return $response ?? new Response(); } /** @@ -164,14 +164,12 @@ protected function emitBeforeControllerInvocation(ActionRequest $request, Contro * returned control back to the dispatcher. * * @param ActionRequest $request - * @param ActionResponse|null $response The response the controller returned or null, if it was just forwarding a request. - * Modifying the response through this signal is not always going to take effect - * and might be ignored for example if the dispatcher is still in the loop. + * @param ResponseInterface|null $response The readonly response the controller returned or null, if it was just forwarding a request. * @param ControllerInterface $controller * @return void * @Flow\Signal */ - protected function emitAfterControllerInvocation(ActionRequest $request, ?ActionResponse $response, ControllerInterface $controller) + protected function emitAfterControllerInvocation(ActionRequest $request, ?ResponseInterface $response, ControllerInterface $controller) { } diff --git a/Neos.Flow/Classes/Mvc/Exception/StopActionException.php b/Neos.Flow/Classes/Mvc/Exception/StopActionException.php index 50a4d6d9b9..48ebdccbb7 100644 --- a/Neos.Flow/Classes/Mvc/Exception/StopActionException.php +++ b/Neos.Flow/Classes/Mvc/Exception/StopActionException.php @@ -11,7 +11,7 @@ * source code. */ -use Neos\Flow\Mvc\ActionResponse; +use Psr\Http\Message\ResponseInterface; use Neos\Flow\Mvc\Controller\AbstractController; /** @@ -31,19 +31,19 @@ final class StopActionException extends \Neos\Flow\Mvc\Exception /** * The response to be received by the MVC Dispatcher. */ - public readonly ActionResponse $response; + public readonly ResponseInterface $response; - private function __construct(string $message, int $code, ?\Throwable $previous, ActionResponse $response) + private function __construct(string $message, int $code, ?\Throwable $previous, ResponseInterface $response) { parent::__construct($message, $code, $previous); $this->response = $response; } /** - * @param ActionResponse $response The response to be received by the MVC Dispatcher. + * @param ResponseInterface $response The response to be received by the MVC Dispatcher. * @param string $details Additional details just for this exception, in case it is logged (the regular exception message). */ - public static function createForResponse(ActionResponse $response, string $details): self + public static function createForResponse(ResponseInterface $response, string $details): self { if (empty($details)) { $details = sprintf( diff --git a/Neos.Flow/Classes/Mvc/Routing/ConfigurationRoutesProvider.php b/Neos.Flow/Classes/Mvc/Routing/ConfigurationRoutesProvider.php new file mode 100644 index 0000000000..f876008e8c --- /dev/null +++ b/Neos.Flow/Classes/Mvc/Routing/ConfigurationRoutesProvider.php @@ -0,0 +1,27 @@ +configurationManager = $configurationManager; + } + + public function getRoutes(): Routes + { + return Routes::fromConfiguration($this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_ROUTES)); + } +} diff --git a/Neos.Flow/Classes/Mvc/Routing/Route.php b/Neos.Flow/Classes/Mvc/Routing/Route.php index 2f60eedc2a..0b073796b9 100644 --- a/Neos.Flow/Classes/Mvc/Routing/Route.php +++ b/Neos.Flow/Classes/Mvc/Routing/Route.php @@ -31,6 +31,7 @@ /** * Implementation of a standard route + * @phpstan-consistent-constructor */ class Route { @@ -172,6 +173,41 @@ class Route */ protected $persistenceManager; + public static function fromConfiguration(array $configuration): static + { + /** @phpstan-ignore-next-line phpstan doesn't respekt the consistent constructor flag in the class doc block */ + $route = new static(); + if (isset($configuration['name'])) { + $route->setName($configuration['name']); + } + $uriPattern = $configuration['uriPattern']; + $route->setUriPattern($uriPattern); + if (isset($configuration['defaults'])) { + $route->setDefaults($configuration['defaults']); + } + if (isset($configuration['routeParts'])) { + $route->setRoutePartsConfiguration($configuration['routeParts']); + } + if (isset($configuration['toLowerCase'])) { + $route->setLowerCase($configuration['toLowerCase']); + } + if (isset($configuration['appendExceedingArguments'])) { + $route->setAppendExceedingArguments($configuration['appendExceedingArguments']); + } + if (isset($configuration['cache'])) { + if (isset($configuration['cache']['lifetime'])) { + $route->setCacheLifetime(RouteLifetime::fromInt($configuration['cache']['lifetime'])); + } + if (isset($configuration['cache']['tags']) && !empty($configuration['cache']['lifetime'])) { + $route->setCacheTags(RouteTags::createFromArray($configuration['cache']['tags'])); + } + } + if (isset($configuration['httpMethods'])) { + $route->setHttpMethods($configuration['httpMethods']); + } + return $route; + } + /** * Sets Route name. * diff --git a/Neos.Flow/Classes/Mvc/Routing/Router.php b/Neos.Flow/Classes/Mvc/Routing/Router.php index c66110009d..b4ebe8267c 100644 --- a/Neos.Flow/Classes/Mvc/Routing/Router.php +++ b/Neos.Flow/Classes/Mvc/Routing/Router.php @@ -12,7 +12,6 @@ */ use Neos\Flow\Annotations as Flow; -use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Http\Helper\RequestInformationHelper; use Neos\Flow\Http\Helper\UriHelper; use Neos\Flow\Log\Utility\LogEnvironment; @@ -21,8 +20,6 @@ use Neos\Flow\Mvc\Exception\NoMatchingRouteException; use Neos\Flow\Mvc\Routing\Dto\ResolveContext; use Neos\Flow\Mvc\Routing\Dto\RouteContext; -use Neos\Flow\Mvc\Routing\Dto\RouteLifetime; -use Neos\Flow\Mvc\Routing\Dto\RouteTags; use Psr\Http\Message\UriInterface; use Psr\Log\LoggerInterface; @@ -35,16 +32,16 @@ class Router implements RouterInterface { /** - * @Flow\Inject(name="Neos.Flow:SystemLogger") - * @var LoggerInterface + * @Flow\Inject + * @var RoutesProviderInterface */ - protected $logger; + protected $routesProvider; /** - * @Flow\Inject - * @var ConfigurationManager + * @Flow\Inject(name="Neos.Flow:SystemLogger") + * @var LoggerInterface */ - protected $configurationManager; + protected $logger; /** * @Flow\Inject @@ -52,27 +49,6 @@ class Router implements RouterInterface */ protected $routerCachingService; - /** - * Array containing the configuration for all routes - * - * @var array - */ - protected $routesConfiguration = null; - - /** - * Array of routes to match against - * - * @var array - */ - protected $routes = []; - - /** - * true if route object have been created, otherwise false - * - * @var boolean - */ - protected $routesCreated = false; - /** * @var Route */ @@ -94,25 +70,13 @@ public function injectLogger(LoggerInterface $logger) $this->logger = $logger; } - /** - * Sets the routes configuration. - * - * @param array|null $routesConfiguration The routes configuration or NULL if it should be fetched from configuration - * @return void - */ - public function setRoutesConfiguration(array $routesConfiguration = null) - { - $this->routesConfiguration = $routesConfiguration; - $this->routesCreated = false; - } - /** * Iterates through all configured routes and calls matches() on them. * Returns the matchResults of the matching route or NULL if no matching * route could be found. * * @param RouteContext $routeContext The Route Context containing the current HTTP Request and, optional, Routing RouteParameters - * @return array The results of the matching route + * @return array The results of the matching route or NULL if no route matched * @throws InvalidRouteSetupException * @throws NoMatchingRouteException if no route matched the given $routeContext * @throws InvalidRoutePartValueException @@ -124,10 +88,10 @@ public function route(RouteContext $routeContext): array if ($cachedRouteResult !== false) { return $cachedRouteResult; } - $this->createRoutesFromConfiguration(); + $httpRequest = $routeContext->getHttpRequest(); - foreach ($this->routes as $route) { + foreach ($this->routesProvider->getRoutes() as $route) { if ($route->matches($routeContext) === true) { $this->lastMatchedRoute = $route; $matchResults = $route->getMatchResults(); @@ -152,29 +116,6 @@ public function getLastMatchedRoute() return $this->lastMatchedRoute; } - /** - * Returns a list of configured routes - * - * @return array - */ - public function getRoutes() - { - $this->createRoutesFromConfiguration(); - return $this->routes; - } - - /** - * Manually adds a route to the beginning of the configured routes - * - * @param Route $route - * @return void - */ - public function addRoute(Route $route) - { - $this->createRoutesFromConfiguration(); - array_unshift($this->routes, $route); - } - /** * Builds the corresponding uri (excluding protocol and host) by iterating * through all configured routes and calling their respective resolves() @@ -193,9 +134,7 @@ public function resolve(ResolveContext $resolveContext): UriInterface return $cachedResolvedUriConstraints->applyTo($resolveContext->getBaseUri(), $resolveContext->isForceAbsoluteUri()); } - $this->createRoutesFromConfiguration(); - - foreach ($this->routes as $route) { + foreach ($this->routesProvider->getRoutes() as $route) { if ($route->resolves($resolveContext) === true) { $uriConstraints = $route->getResolvedUriConstraints()->withPathPrefix($resolveContext->getUriPathPrefix()); $resolvedUri = $uriConstraints->applyTo($resolveContext->getBaseUri(), $resolveContext->isForceAbsoluteUri()); @@ -218,75 +157,4 @@ public function getLastResolvedRoute() { return $this->lastResolvedRoute; } - - /** - * Creates \Neos\Flow\Mvc\Routing\Route objects from the injected routes - * configuration. - * - * @return void - * @throws InvalidRouteSetupException - */ - protected function createRoutesFromConfiguration() - { - if ($this->routesCreated === true) { - return; - } - $this->initializeRoutesConfiguration(); - $this->routes = []; - $routesWithHttpMethodConstraints = []; - foreach ($this->routesConfiguration as $routeConfiguration) { - $route = new Route(); - if (isset($routeConfiguration['name'])) { - $route->setName($routeConfiguration['name']); - } - $uriPattern = $routeConfiguration['uriPattern']; - $route->setUriPattern($uriPattern); - if (isset($routeConfiguration['defaults'])) { - $route->setDefaults($routeConfiguration['defaults']); - } - if (isset($routeConfiguration['routeParts'])) { - $route->setRoutePartsConfiguration($routeConfiguration['routeParts']); - } - if (isset($routeConfiguration['toLowerCase'])) { - $route->setLowerCase($routeConfiguration['toLowerCase']); - } - if (isset($routeConfiguration['appendExceedingArguments'])) { - $route->setAppendExceedingArguments($routeConfiguration['appendExceedingArguments']); - } - if (isset($routeConfiguration['httpMethods'])) { - if (isset($routesWithHttpMethodConstraints[$uriPattern]) && $routesWithHttpMethodConstraints[$uriPattern] === false) { - throw new InvalidRouteSetupException(sprintf('There are multiple routes with the uriPattern "%s" and "httpMethods" option set. Please specify accepted HTTP methods for all of these, or adjust the uriPattern', $uriPattern), 1365678427); - } - $routesWithHttpMethodConstraints[$uriPattern] = true; - $route->setHttpMethods($routeConfiguration['httpMethods']); - } else { - if (isset($routesWithHttpMethodConstraints[$uriPattern]) && $routesWithHttpMethodConstraints[$uriPattern] === true) { - throw new InvalidRouteSetupException(sprintf('There are multiple routes with the uriPattern "%s" and "httpMethods" option set. Please specify accepted HTTP methods for all of these, or adjust the uriPattern', $uriPattern), 1365678432); - } - $routesWithHttpMethodConstraints[$uriPattern] = false; - } - if (isset($routeConfiguration['cache'])) { - if (isset($routeConfiguration['cache']['lifetime']) && !is_null($routeConfiguration['cache']['lifetime'])) { - $route->setCacheLifetime(RouteLifetime::fromInt($routeConfiguration['cache']['lifetime'])); - } - if (isset($routeConfiguration['cache']['tags']) && !empty($routeConfiguration['cache']['lifetime'])) { - $route->setCacheTags(RouteTags::createFromArray($routeConfiguration['cache']['tags'])); - } - } - $this->routes[] = $route; - } - $this->routesCreated = true; - } - - /** - * Checks if a routes configuration was set and otherwise loads the configuration from the configuration manager. - * - * @return void - */ - protected function initializeRoutesConfiguration() - { - if ($this->routesConfiguration === null) { - $this->routesConfiguration = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_ROUTES); - } - } } diff --git a/Neos.Flow/Classes/Mvc/Routing/Routes.php b/Neos.Flow/Classes/Mvc/Routing/Routes.php new file mode 100644 index 0000000000..cc0822c971 --- /dev/null +++ b/Neos.Flow/Classes/Mvc/Routing/Routes.php @@ -0,0 +1,75 @@ + + */ +final class Routes implements \IteratorAggregate +{ + /** + * @var array + */ + private array $routes; + + private function __construct( + Route ...$routes + ) { + $this->routes = $routes; + + // validate that each route is unique + $routesWithHttpMethodConstraints = []; + foreach ($this->routes as $route) { + $uriPattern = $route->getUriPattern(); + if ($route->hasHttpMethodConstraints()) { + if (isset($routesWithHttpMethodConstraints[$uriPattern]) && $routesWithHttpMethodConstraints[$uriPattern] === false) { + throw new InvalidRouteSetupException(sprintf('There are multiple routes with the uriPattern "%s" and "httpMethods" option set. Please specify accepted HTTP methods for all of these, or adjust the uriPattern', $uriPattern), 1365678427); + } + $routesWithHttpMethodConstraints[$uriPattern] = true; + } else { + if (isset($routesWithHttpMethodConstraints[$uriPattern]) && $routesWithHttpMethodConstraints[$uriPattern] === true) { + throw new InvalidRouteSetupException(sprintf('There are multiple routes with the uriPattern "%s" and "httpMethods" option set. Please specify accepted HTTP methods for all of these, or adjust the uriPattern', $uriPattern), 1365678432); + } + $routesWithHttpMethodConstraints[$uriPattern] = false; + } + } + } + + public static function create(Route ...$routes): self + { + return new self(...$routes); + } + + public static function fromConfiguration(array $configuration): self + { + $routes = []; + foreach ($configuration as $routeConfiguration) { + $routes[] = Route::fromConfiguration($routeConfiguration); + } + return new self(...$routes); + } + + public static function empty(): self + { + return new self(); + } + + public function merge(Routes $other): self + { + return new self(...$this->routes, ...$other->routes); + } + + /** + * @return \Traversable + */ + public function getIterator(): Traversable + { + yield from $this->routes; + } +} diff --git a/Neos.Flow/Classes/Mvc/Routing/RoutesProviderInterface.php b/Neos.Flow/Classes/Mvc/Routing/RoutesProviderInterface.php new file mode 100644 index 0000000000..5cd3f98ae6 --- /dev/null +++ b/Neos.Flow/Classes/Mvc/Routing/RoutesProviderInterface.php @@ -0,0 +1,18 @@ +additionalRoutes = Routes::empty(); + } + + /** + * Prepends a route additionally to the routes form the Testing context configuration + * + * @internal Please use {@see FunctionalTestCase::registerRoute} instead. + */ + public function addRoute(Route $route) + { + // we prepended the route, like the old Router::addRoute + $this->additionalRoutes = Routes::create($route)->merge($this->additionalRoutes); + } + + public function reset(): void + { + $this->additionalRoutes = Routes::empty(); + } + + public function getRoutes(): Routes + { + // we prepended all additional routes, like the old Router::addRoute + return $this->additionalRoutes->merge( + $this->configurationRoutesProvider->getRoutes() + ); + } +} diff --git a/Neos.Flow/Classes/Mvc/Routing/UriBuilder.php b/Neos.Flow/Classes/Mvc/Routing/UriBuilder.php index e90eb50c03..23c60aad51 100644 --- a/Neos.Flow/Classes/Mvc/Routing/UriBuilder.php +++ b/Neos.Flow/Classes/Mvc/Routing/UriBuilder.php @@ -279,7 +279,7 @@ public function reset() * Creates an URI used for linking to an Controller action. * * @param string $actionName Name of the action to be called - * @param array $controllerArguments Additional query parameters. Will be merged with $this->arguments. + * @param array $controllerArguments Additional routing arguments. Will be merged with $this->arguments. * @param string|null $controllerName Name of the target controller. If not set, current ControllerName is used. * @param string|null $packageKey Name of the target package. If not set, current Package is used. * @param string|null $subPackageKey Name of the target SubPackage. If not set, current SubPackage is used. diff --git a/Neos.Flow/Classes/Mvc/View/ViewInterface.php b/Neos.Flow/Classes/Mvc/View/ViewInterface.php index 45f544256a..7dca0cff94 100644 --- a/Neos.Flow/Classes/Mvc/View/ViewInterface.php +++ b/Neos.Flow/Classes/Mvc/View/ViewInterface.php @@ -63,7 +63,9 @@ public function canRender(ControllerContext $controllerContext); /** * Renders the view * - * @return string|ActionResponse|ResponseInterface|StreamInterface|object The rendered result; object is only handled if __toString() exists! + * Returning an {@see ActionResponse} is deprecated. + * + * @return string|ActionResponse|ResponseInterface|StreamInterface|\Stringable The rendered result * @api */ public function render(); diff --git a/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php b/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php index 91712d6b7f..106d21275b 100644 --- a/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php +++ b/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php @@ -11,14 +11,18 @@ * source code. */ +use Neos\Cache\Exception as CacheException; use Neos\Cache\Frontend\VariableFrontend; -use Neos\Flow\Composer\ComposerUtility as ComposerUtility; +use Neos\Flow\Composer\ComposerUtility; use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException; use Neos\Flow\ObjectManagement\Configuration\Configuration; use Neos\Flow\ObjectManagement\Configuration\ConfigurationBuilder; use Neos\Flow\ObjectManagement\Configuration\ConfigurationProperty as Property; use Neos\Flow\Annotations as Flow; +use Neos\Flow\ObjectManagement\Exception\InvalidObjectConfigurationException; +use Neos\Flow\ObjectManagement\Exception\UnknownObjectException; +use Neos\Flow\ObjectManagement\Exception\WrongScopeException; use Neos\Flow\Package\FlowPackageInterface; use Neos\Flow\Package\PackageInterface; use Neos\Flow\Reflection\ReflectionService; @@ -35,52 +39,52 @@ class CompileTimeObjectManager extends ObjectManager { /** - * @var VariableFrontend + * @var VariableFrontend|null */ - protected $configurationCache; + protected ?VariableFrontend $configurationCache; /** - * @var ReflectionService + * @var ReflectionService|null */ - protected $reflectionService; + protected ?ReflectionService $reflectionService; /** - * @var ConfigurationManager + * @var ConfigurationManager|null */ - protected $configurationManager; + protected ?ConfigurationManager $configurationManager; /** - * @var LoggerInterface + * @var LoggerInterface|null */ - protected $logger; + protected ?LoggerInterface $logger; /** * @var array */ - protected $objectConfigurations; + protected array $objectConfigurations = []; /** * A list of all class names known to the Object Manager * * @var array */ - protected $registeredClassNames = []; + protected array $registeredClassNames = []; /** * @var array */ - protected $objectNameBuildStack = []; + protected array $objectNameBuildStack = []; /** * @var array */ - protected $cachedClassNamesByScope = []; + protected array $cachedClassNamesByScope = []; /** * @param ReflectionService $reflectionService * @return void */ - public function injectReflectionService(ReflectionService $reflectionService) + public function injectReflectionService(ReflectionService $reflectionService): void { $this->reflectionService = $reflectionService; } @@ -89,7 +93,7 @@ public function injectReflectionService(ReflectionService $reflectionService) * @param ConfigurationManager $configurationManager * @return void */ - public function injectConfigurationManager(ConfigurationManager $configurationManager) + public function injectConfigurationManager(ConfigurationManager $configurationManager): void { $this->configurationManager = $configurationManager; } @@ -100,7 +104,7 @@ public function injectConfigurationManager(ConfigurationManager $configurationMa * @param VariableFrontend $configurationCache * @return void */ - public function injectConfigurationCache(VariableFrontend $configurationCache) + public function injectConfigurationCache(VariableFrontend $configurationCache): void { $this->configurationCache = $configurationCache; } @@ -111,7 +115,7 @@ public function injectConfigurationCache(VariableFrontend $configurationCache) * @param LoggerInterface $logger * @return void */ - public function injectLogger(LoggerInterface $logger) + public function injectLogger(LoggerInterface $logger): void { $this->logger = $logger; } @@ -121,8 +125,11 @@ public function injectLogger(LoggerInterface $logger) * * @param PackageInterface[] $packages An array of active packages to consider * @return void + * @throws InvalidConfigurationTypeException + * @throws InvalidObjectConfigurationException + * @throws CacheException */ - public function initialize(array $packages) + public function initialize(array $packages): void { $this->registeredClassNames = $this->registerClassFiles($packages); $this->reflectionService->buildReflectionData($this->registeredClassNames); @@ -149,11 +156,13 @@ public function initialize(array $packages) * @param string $objectName The object name * @param object $instance A prebuilt instance * @return void + * @throws UnknownObjectException + * @throws WrongScopeException */ - public function setInstance($objectName, $instance) + public function setInstance($objectName, $instance): void { if ($this->registeredClassNames === []) { - $this->objects[$objectName]['i'] = $instance; + $this->objects[$objectName][self::KEY_INSTANCE] = $instance; } else { parent::setInstance($objectName, $instance); } @@ -164,7 +173,7 @@ public function setInstance($objectName, $instance) * * @return array */ - public function getRegisteredClassNames() + public function getRegisteredClassNames(): array { return $this->registeredClassNames; } @@ -175,13 +184,13 @@ public function getRegisteredClassNames() * @param integer $scope One of the ObjectConfiguration::SCOPE_ constants * @return array An array of class names configured with the given scope */ - public function getClassNamesByScope($scope) + public function getClassNamesByScope(int $scope): array { if (!isset($this->cachedClassNamesByScope[$scope])) { foreach ($this->objects as $objectName => $information) { - if ($information['s'] === $scope) { - if (isset($information['c'])) { - $this->cachedClassNamesByScope[$scope][] = $information['c']; + if ($information[self::KEY_SCOPE] === $scope) { + if (isset($information[self::KEY_CLASS_NAME])) { + $this->cachedClassNamesByScope[$scope][] = $information[self::KEY_CLASS_NAME]; } else { $this->cachedClassNamesByScope[$scope][] = $objectName; } @@ -203,7 +212,7 @@ public function getClassNamesByScope($scope) * * @throws InvalidConfigurationTypeException */ - protected function registerClassFiles(array $packages) + protected function registerClassFiles(array $packages): array { $includeClassesConfiguration = []; if (isset($this->allSettings['Neos']['Flow']['object']['includeClasses'])) { @@ -218,24 +227,23 @@ protected function registerClassFiles(array $packages) $shouldRegisterFunctionalTestClasses = (bool)($this->allSettings['Neos']['Flow']['object']['registerFunctionalTestClasses'] ?? false); - /** @var \Neos\Flow\Package\PackageInterface $package */ foreach ($packages as $packageKey => $package) { $packageType = (string)$package->getComposerManifest('type'); - if (ComposerUtility::isFlowPackageType($packageType) || isset($includeClassesConfiguration[$packageKey])) { + if (isset($includeClassesConfiguration[$packageKey]) || ComposerUtility::isFlowPackageType($packageType)) { foreach ($package->getClassFiles() as $fullClassName => $path) { - if (substr($fullClassName, -9, 9) !== 'Exception') { + if (!str_ends_with($fullClassName, 'Exception')) { $availableClassNames[$packageKey][] = $fullClassName; } } if ($package instanceof FlowPackageInterface && $shouldRegisterFunctionalTestClasses) { foreach ($package->getFunctionalTestsClassFiles() as $fullClassName => $path) { - if (version_compare(PHP_VERSION, '8.0', '<=') && strpos($fullClassName, '\\PHP8\\') !== false) { + if (PHP_VERSION_ID <= 80000 && str_contains($fullClassName, '\\PHP8\\')) { continue; } - if (version_compare(PHP_VERSION, '8.1', '<=') && strpos($fullClassName, '\\PHP81\\') !== false) { + if (PHP_VERSION_ID <= 80100 && str_contains($fullClassName, '\\PHP81\\')) { continue; } - if (substr($fullClassName, -9, 9) !== 'Exception') { + if (!str_ends_with($fullClassName, 'Exception')) { $availableClassNames[$packageKey][] = $fullClassName; } } @@ -256,12 +264,10 @@ protected function registerClassFiles(array $packages) * @param array $includeClassesConfiguration array of includeClasses configurations * @return array The input array with all configured to be included in object management added in * @throws InvalidConfigurationTypeException - * @throws \Neos\Flow\Configuration\Exception\NoSuchOptionException */ - protected function filterClassNamesFromConfiguration(array $classNames, $includeClassesConfiguration) + protected function filterClassNamesFromConfiguration(array $classNames, array $includeClassesConfiguration): array { - $classNames = $this->applyClassFilterConfiguration($classNames, $includeClassesConfiguration); - return $classNames; + return $this->applyClassFilterConfiguration($classNames, $includeClassesConfiguration); } /** @@ -272,7 +278,7 @@ protected function filterClassNamesFromConfiguration(array $classNames, $include * @return array the remaining class * @throws InvalidConfigurationTypeException */ - protected function applyClassFilterConfiguration($classNames, $filterConfiguration) + protected function applyClassFilterConfiguration(array $classNames, array $filterConfiguration): array { foreach ($filterConfiguration as $packageKey => $filterExpressions) { if (!array_key_exists($packageKey, $classNames)) { @@ -289,7 +295,7 @@ protected function applyClassFilterConfiguration($classNames, $filterConfigurati foreach ($filterExpressions as $filterExpression) { $classesForPackageUnderInspection = array_filter( $classesForPackageUnderInspection, - function ($className) use ($filterExpression) { + static function ($className) use ($filterExpression) { $match = preg_match('/' . $filterExpression . '/', $className); return $match === 1; } @@ -311,34 +317,35 @@ function ($className) use ($filterExpression) { * their scope, class, built method etc. * * @return array + * @throws CacheException */ - protected function buildObjectsArray() + protected function buildObjectsArray(): array { $objects = []; /* @var $objectConfiguration Configuration */ foreach ($this->objectConfigurations as $objectConfiguration) { $objectName = $objectConfiguration->getObjectName(); $objects[$objectName] = [ - 'l' => strtolower($objectName), - 's' => $objectConfiguration->getScope(), - 'p' => $objectConfiguration->getPackageKey() + self::KEY_LOWERCASE_NAME => strtolower($objectName), + self::KEY_SCOPE => $objectConfiguration->getScope(), + self::KEY_PACKAGE => $objectConfiguration->getPackageKey() ]; if ($objectConfiguration->getClassName() !== $objectName) { - $objects[$objectName]['c'] = $objectConfiguration->getClassName(); + $objects[$objectName][self::KEY_CLASS_NAME] = $objectConfiguration->getClassName(); } if ($objectConfiguration->isCreatedByFactory()) { - $objects[$objectName]['f'] = [ + $objects[$objectName][self::KEY_FACTORY] = [ $objectConfiguration->getFactoryObjectName(), $objectConfiguration->getFactoryMethodName() ]; - $objects[$objectName]['fa'] = []; + $objects[$objectName][self::KEY_FACTORY_ARGUMENTS] = []; $factoryMethodArguments = $objectConfiguration->getFactoryArguments(); if (count($factoryMethodArguments) > 0) { foreach ($factoryMethodArguments as $index => $argument) { - $objects[$objectName]['fa'][$index] = [ - 't' => $argument->getType(), - 'v' => $argument->getValue() + $objects[$objectName][self::KEY_FACTORY_ARGUMENTS][$index] = [ + self::KEY_ARGUMENT_TYPE => $argument->getType(), + self::KEY_ARGUMENT_VALUE => $argument->getValue() ]; } } @@ -353,7 +360,7 @@ protected function buildObjectsArray() * * @return array */ - public function getObjectConfigurations() + public function getObjectConfigurations(): array { return $this->objectConfigurations; } @@ -369,14 +376,15 @@ public function getObjectConfigurations() * @param mixed ...$constructorArguments Any number of arguments that should be passed to the constructor of the object * @phpstan-return ($objectName is class-string ? T : object) The object instance * @return T The object instance - * @throws \Neos\Flow\ObjectManagement\Exception\CannotBuildObjectException - * @throws \Neos\Flow\ObjectManagement\Exception\UnresolvedDependenciesException - * @throws \Neos\Flow\ObjectManagement\Exception\UnknownObjectException + * @throws Exception\CannotBuildObjectException + * @throws Exception\UnresolvedDependenciesException + * @throws Exception\UnknownObjectException + * @throws InvalidConfigurationTypeException */ - public function get($objectName, ...$constructorArguments) + public function get($objectName, ...$constructorArguments): object { - if (isset($this->objects[$objectName]['i'])) { - return $this->objects[$objectName]['i']; + if (isset($this->objects[$objectName][self::KEY_INSTANCE])) { + return $this->objects[$objectName][self::KEY_INSTANCE]; } if (isset($this->objectConfigurations[$objectName]) && count($this->objectConfigurations[$objectName]->getArguments()) > 0) { @@ -386,7 +394,7 @@ public function get($objectName, ...$constructorArguments) throw new Exception\UnknownObjectException('Cannot build object "' . $objectName . '" because it is unknown to the compile time Object Manager.', 1301477694); } - if ($this->objects[$objectName]['s'] !== Configuration::SCOPE_SINGLETON) { + if ($this->objects[$objectName][self::KEY_SCOPE] !== Configuration::SCOPE_SINGLETON) { throw new Exception\CannotBuildObjectException('Cannot build object "' . $objectName . '" because the get() method in the compile time Object Manager only supports singletons.', 1297090027); } @@ -395,7 +403,6 @@ public function get($objectName, ...$constructorArguments) $object = parent::get($objectName); /** @var Configuration $objectConfiguration */ $objectConfiguration = $this->objectConfigurations[$objectName]; - /** @var Property $property */ foreach ($objectConfiguration->getProperties() as $propertyName => $property) { if ($property->getAutowiring() !== Configuration::AUTOWIRING_MODE_ON) { continue; @@ -448,7 +455,7 @@ public function get($objectName, ...$constructorArguments) * * @return void */ - public function shutdown() + public function shutdown(): void { $this->callShutdownMethods($this->shutdownObjects); $this->callShutdownMethods($this->internalShutdownObjects); diff --git a/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php b/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php index cb591332d7..89296604c7 100644 --- a/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php +++ b/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php @@ -113,7 +113,7 @@ class Configuration */ public function __construct($objectName, $className = null) { - $backtrace = debug_backtrace(); + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); if (isset($backtrace[1]['object'])) { $this->configurationSourceHint = get_class($backtrace[1]['object']); } elseif (isset($backtrace[1]['class'])) { diff --git a/Neos.Flow/Classes/ObjectManagement/ObjectManager.php b/Neos.Flow/Classes/ObjectManagement/ObjectManager.php index 0e662cdd70..6a03f2c458 100644 --- a/Neos.Flow/Classes/ObjectManagement/ObjectManager.php +++ b/Neos.Flow/Classes/ObjectManagement/ObjectManager.php @@ -30,39 +30,49 @@ */ class ObjectManager implements ObjectManagerInterface { + protected const KEY_INSTANCE = 'i'; + protected const KEY_SCOPE = 's'; + protected const KEY_FACTORY = 'f'; + protected const KEY_FACTORY_ARGUMENTS = 'fa'; + protected const KEY_ARGUMENT_TYPE = 't'; + protected const KEY_ARGUMENT_VALUE = 'v'; + protected const KEY_CLASS_NAME = 'c'; + protected const KEY_PACKAGE = 'p'; + protected const KEY_LOWERCASE_NAME = 'l'; + /** * The configuration context for this Flow run * * @var ApplicationContext */ - protected $context; + protected ApplicationContext $context; /** * An array of settings of all packages, indexed by package key * * @var array */ - protected $allSettings; + protected array $allSettings = []; /** * @var array */ - protected $objects = []; + protected array $objects = []; /** * @var array */ - protected $dependencyProxies = []; + protected array $dependencyProxies = []; /** * @var array */ - protected $classesBeingInstantiated = []; + protected array $classesBeingInstantiated = []; /** * @var array */ - protected $cachedLowerCasedObjectNames = []; + protected array $cachedLowerCasedObjectNames = []; /** * A SplObjectStorage containing those objects which need to be shutdown when the container @@ -70,7 +80,7 @@ class ObjectManager implements ObjectManagerInterface * * @var \SplObjectStorage */ - protected $shutdownObjects; + protected \SplObjectStorage $shutdownObjects; /** * A SplObjectStorage containing only those shutdown objects which have been registered for Flow. @@ -78,7 +88,7 @@ class ObjectManager implements ObjectManagerInterface * * @var \SplObjectStorage */ - protected $internalShutdownObjects; + protected \SplObjectStorage $internalShutdownObjects; /** * Constructor for this Object Container @@ -101,8 +111,8 @@ public function __construct(ApplicationContext $context) public function setObjects(array $objects): void { $this->objects = $objects; - $this->objects[ObjectManagerInterface::class]['i'] = $this; - $this->objects[get_class($this)]['i'] = $this; + $this->objects[ObjectManagerInterface::class][self::KEY_INSTANCE] = $this; + $this->objects[get_class($this)][self::KEY_INSTANCE] = $this; } /** @@ -122,7 +132,7 @@ public function injectAllSettings(array $settings): void * * @return ApplicationContext The context, for example "Development" or "Production" */ - public function getContext() + public function getContext(): ApplicationContext { return $this->context; } @@ -135,7 +145,7 @@ public function getContext() * @throws \InvalidArgumentException * @api */ - public function isRegistered($objectName) + public function isRegistered($objectName): bool { if (isset($this->objects[$objectName])) { return true; @@ -154,7 +164,7 @@ public function isRegistered($objectName) * @param string $objectName * @return bool */ - public function has($objectName) + public function has($objectName): bool { return $this->isRegistered($objectName); } @@ -167,9 +177,9 @@ public function has($objectName) * @return void * @api */ - public function registerShutdownObject($object, $shutdownLifecycleMethodName) + public function registerShutdownObject($object, $shutdownLifecycleMethodName): void { - if (strpos(get_class($object), 'Neos\Flow\\') === 0) { + if (str_starts_with(get_class($object), 'Neos\Flow\\')) { $this->internalShutdownObjects[$object] = $shutdownLifecycleMethodName; } else { $this->shutdownObjects[$object] = $shutdownLifecycleMethodName; @@ -190,23 +200,23 @@ public function registerShutdownObject($object, $shutdownLifecycleMethodName) * @throws InvalidConfigurationTypeException * @api */ - public function get($objectName, ...$constructorArguments) + public function get($objectName, ...$constructorArguments): object { - if (!empty($constructorArguments) && isset($this->objects[$objectName]) && $this->objects[$objectName]['s'] !== ObjectConfiguration::SCOPE_PROTOTYPE) { + if (!empty($constructorArguments) && isset($this->objects[$objectName]) && $this->objects[$objectName][self::KEY_SCOPE] !== ObjectConfiguration::SCOPE_PROTOTYPE) { throw new \InvalidArgumentException('You cannot provide constructor arguments for singleton objects via get(). If you need to pass arguments to the constructor, define them in the Objects.yaml configuration.', 1298049934); } - if (isset($this->objects[$objectName]['i'])) { - return $this->objects[$objectName]['i']; + if (isset($this->objects[$objectName][self::KEY_INSTANCE])) { + return $this->objects[$objectName][self::KEY_INSTANCE]; } - if (isset($this->objects[$objectName]['f'])) { - if ($this->objects[$objectName]['s'] === ObjectConfiguration::SCOPE_PROTOTYPE) { + if (isset($this->objects[$objectName][self::KEY_FACTORY])) { + if ($this->objects[$objectName][self::KEY_SCOPE] === ObjectConfiguration::SCOPE_PROTOTYPE) { return $this->buildObjectByFactory($objectName); } - $this->objects[$objectName]['i'] = $this->buildObjectByFactory($objectName); - return $this->objects[$objectName]['i']; + $this->objects[$objectName][self::KEY_INSTANCE] = $this->buildObjectByFactory($objectName); + return $this->objects[$objectName][self::KEY_INSTANCE]; } $className = $this->getClassNameByObjectName($objectName); @@ -215,12 +225,12 @@ public function get($objectName, ...$constructorArguments) throw new Exception\UnknownObjectException('Object "' . $objectName . '" is not registered.' . $hint, 1264589155); } - if (!isset($this->objects[$objectName]) || $this->objects[$objectName]['s'] === ObjectConfiguration::SCOPE_PROTOTYPE) { + if (!isset($this->objects[$objectName]) || $this->objects[$objectName][self::KEY_SCOPE] === ObjectConfiguration::SCOPE_PROTOTYPE) { return $this->instantiateClass($className, $constructorArguments); } - $this->objects[$objectName]['i'] = $this->instantiateClass($className, []); - return $this->objects[$objectName]['i']; + $this->objects[$objectName][self::KEY_INSTANCE] = $this->instantiateClass($className, []); + return $this->objects[$objectName][self::KEY_INSTANCE]; } /** @@ -231,13 +241,13 @@ public function get($objectName, ...$constructorArguments) * @throws Exception\UnknownObjectException * @api */ - public function getScope($objectName) + public function getScope($objectName): int { if (!isset($this->objects[$objectName])) { $hint = ($objectName[0] === '\\') ? ' Hint: You specified an object name with a leading backslash!' : ''; throw new Exception\UnknownObjectException('Object "' . $objectName . '" is not registered.' . $hint, 1265367590); } - return $this->objects[$objectName]['s']; + return $this->objects[$objectName][self::KEY_SCOPE]; } /** @@ -262,7 +272,7 @@ public function getCaseSensitiveObjectName($caseInsensitiveObjectName): ?string } foreach ($this->objects as $objectName => $information) { - if (isset($information['l']) && $information['l'] === $lowerCasedObjectName) { + if (isset($information[self::KEY_LOWERCASE_NAME]) && $information[self::KEY_LOWERCASE_NAME] === $lowerCasedObjectName) { $this->cachedLowerCasedObjectNames[$lowerCasedObjectName] = $objectName; return $objectName; } @@ -282,14 +292,14 @@ public function getCaseSensitiveObjectName($caseInsensitiveObjectName): ?string * * @api */ - public function getObjectNameByClassName($className) + public function getObjectNameByClassName($className): string|false { - if (isset($this->objects[$className]) && (!isset($this->objects[$className]['c']) || $this->objects[$className]['c'] === $className)) { + if (isset($this->objects[$className]) && (!isset($this->objects[$className][self::KEY_CLASS_NAME]) || $this->objects[$className][self::KEY_CLASS_NAME] === $className)) { return $className; } foreach ($this->objects as $objectName => $information) { - if (isset($information['c']) && $information['c'] === $className) { + if (isset($information[self::KEY_CLASS_NAME]) && $information[self::KEY_CLASS_NAME] === $className) { return $objectName; } } @@ -304,27 +314,27 @@ public function getObjectNameByClassName($className) * Returns the implementation class name for the specified object * * @param string $objectName The object name - * @return string The class name corresponding to the given object name or false if no such object is registered + * @return string|false The class name corresponding to the given object name or false if no such object is registered * @api */ - public function getClassNameByObjectName($objectName) + public function getClassNameByObjectName($objectName): string|false { if (!isset($this->objects[$objectName])) { return class_exists($objectName) ? $objectName : false; } - return $this->objects[$objectName]['c'] ?? $objectName; + return $this->objects[$objectName][self::KEY_CLASS_NAME] ?? $objectName; } /** * Returns the key of the package the specified object is contained in. * * @param string $objectName The object name - * @return string The package key or false if no such object exists + * @return string|false The package key or false if no such object exists * @internal */ - public function getPackageKeyByObjectName($objectName) + public function getPackageKeyByObjectName($objectName): string|false { - return (isset($this->objects[$objectName]) ? $this->objects[$objectName]['p'] : false); + return (isset($this->objects[$objectName]) ? $this->objects[$objectName][self::KEY_PACKAGE] : false); } /** @@ -339,19 +349,19 @@ public function getPackageKeyByObjectName($objectName) * @throws Exception\WrongScopeException * @throws Exception\UnknownObjectException */ - public function setInstance($objectName, $instance) + public function setInstance($objectName, $instance): void { if (!isset($this->objects[$objectName])) { if (!class_exists($objectName, false)) { throw new Exception\UnknownObjectException('Cannot set instance of object "' . $objectName . '" because the object or class name is unknown to the Object Manager.', 1265370539); - } else { - throw new Exception\WrongScopeException('Cannot set instance of class "' . $objectName . '" because no matching object configuration was found. Classes which exist but are not registered are considered to be of scope prototype. However, setInstance() only accepts "session" and "singleton" instances. Check your object configuration and class name spellings.', 12653705341); } + + throw new Exception\WrongScopeException('Cannot set instance of class "' . $objectName . '" because no matching object configuration was found. Classes which exist but are not registered are considered to be of scope prototype. However, setInstance() only accepts "session" and "singleton" instances. Check your object configuration and class name spellings.', 12653705341); } - if ($this->objects[$objectName]['s'] === ObjectConfiguration::SCOPE_PROTOTYPE) { + if ($this->objects[$objectName][self::KEY_SCOPE] === ObjectConfiguration::SCOPE_PROTOTYPE) { throw new Exception\WrongScopeException('Cannot set instance of object "' . $objectName . '" because it is of scope prototype. Only session and singleton instances can be set.', 1265370540); } - $this->objects[$objectName]['i'] = $instance; + $this->objects[$objectName][self::KEY_INSTANCE] = $instance; } /** @@ -361,9 +371,9 @@ public function setInstance($objectName, $instance) * @param string $objectName The object name * @return boolean true if an instance already exists */ - public function hasInstance($objectName): bool + public function hasInstance(string $objectName): bool { - return isset($this->objects[$objectName]['i']); + return isset($this->objects[$objectName][self::KEY_INSTANCE]); } /** @@ -375,9 +385,9 @@ public function hasInstance($objectName): bool * @phpstan-return ($objectName is class-string ? T|null : object|null) The object instance or null * @return T|null The object instance or null */ - public function getInstance($objectName) + public function getInstance(string $objectName): ?object { - return $this->objects[$objectName]['i'] ?? null; + return $this->objects[$objectName][self::KEY_INSTANCE] ?? null; } /** @@ -390,9 +400,9 @@ public function getInstance($objectName) * * @param string $hash * @param mixed &$propertyReferenceVariable Reference of the variable to inject into once the proxy is activated - * @return mixed + * @return object|null */ - public function getLazyDependencyByHash($hash, &$propertyReferenceVariable) + public function getLazyDependencyByHash(string $hash, mixed &$propertyReferenceVariable): ?object { if (!isset($this->dependencyProxies[$hash])) { return null; @@ -410,12 +420,12 @@ public function getLazyDependencyByHash($hash, &$propertyReferenceVariable) * Internally used by the injectProperties method of generated proxy classes. * * @param string $hash An md5 hash over the code needed to actually build the dependency instance - * @param string &$propertyReferenceVariable A first variable where the dependency needs to be injected into + * @param mixed &$propertyReferenceVariable A first variable where the dependency needs to be injected into * @param string $className Name of the class of the dependency which eventually will be instantiated * @param \Closure $builder An anonymous function which creates the instance to be injected * @return DependencyProxy */ - public function createLazyDependency($hash, &$propertyReferenceVariable, $className, \Closure $builder): DependencyProxy + public function createLazyDependency(string $hash, mixed &$propertyReferenceVariable, string $className, \Closure $builder): DependencyProxy { $this->dependencyProxies[$hash] = new DependencyProxy($className, $builder); $this->dependencyProxies[$hash]->_addPropertyVariable($propertyReferenceVariable); @@ -433,9 +443,9 @@ public function createLazyDependency($hash, &$propertyReferenceVariable, $classN * @param string $objectName The object name * @return void */ - public function forgetInstance($objectName) + public function forgetInstance($objectName): void { - unset($this->objects[$objectName]['i']); + unset($this->objects[$objectName][self::KEY_INSTANCE]); } /** @@ -443,12 +453,12 @@ public function forgetInstance($objectName) * * @return array */ - public function getSessionInstances() + public function getSessionInstances(): array { $sessionObjects = []; foreach ($this->objects as $information) { - if (isset($information['i']) && $information['s'] === ObjectConfiguration::SCOPE_SESSION) { - $sessionObjects[] = $information['i']; + if (isset($information[self::KEY_INSTANCE]) && $information[self::KEY_SCOPE] === ObjectConfiguration::SCOPE_SESSION) { + $sessionObjects[] = $information[self::KEY_INSTANCE]; } } return $sessionObjects; @@ -462,8 +472,9 @@ public function getSessionInstances() * @throws Exception\CannotBuildObjectException * @throws Exception\UnknownObjectException * @throws InvalidConfigurationTypeException + * @throws \Exception */ - public function shutdown() + public function shutdown(): void { $this->callShutdownMethods($this->shutdownObjects); @@ -500,22 +511,22 @@ public function getAllObjectConfigurations(): array * @throws InvalidConfigurationTypeException * @throws Exception\CannotBuildObjectException */ - protected function buildObjectByFactory($objectName) + protected function buildObjectByFactory(string $objectName): object { - $factory = $this->objects[$objectName]['f'][0] ? $this->get($this->objects[$objectName]['f'][0]) : null; - $factoryMethodName = $this->objects[$objectName]['f'][1]; + $factory = $this->objects[$objectName][self::KEY_FACTORY][0] ? $this->get($this->objects[$objectName][self::KEY_FACTORY][0]) : null; + $factoryMethodName = $this->objects[$objectName][self::KEY_FACTORY][1]; $factoryMethodArguments = []; - foreach ($this->objects[$objectName]['fa'] as $index => $argumentInformation) { - switch ($argumentInformation['t']) { + foreach ($this->objects[$objectName][self::KEY_FACTORY_ARGUMENTS] as $index => $argumentInformation) { + switch ($argumentInformation[self::KEY_ARGUMENT_TYPE]) { case ObjectConfigurationArgument::ARGUMENT_TYPES_SETTING: - $factoryMethodArguments[$index] = $this->get(ConfigurationManager::class)->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, $argumentInformation['v']); + $factoryMethodArguments[$index] = $this->get(ConfigurationManager::class)->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, $argumentInformation[self::KEY_ARGUMENT_VALUE]); break; case ObjectConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE: - $factoryMethodArguments[$index] = $argumentInformation['v']; + $factoryMethodArguments[$index] = $argumentInformation[self::KEY_ARGUMENT_VALUE]; break; case ObjectConfigurationArgument::ARGUMENT_TYPES_OBJECT: - $factoryMethodArguments[$index] = $this->get($argumentInformation['v']); + $factoryMethodArguments[$index] = $this->get($argumentInformation[self::KEY_ARGUMENT_VALUE]); break; } } @@ -536,7 +547,7 @@ protected function buildObjectByFactory($objectName) * @throws Exception\CannotBuildObjectException * @throws \Exception */ - protected function instantiateClass($className, array $arguments) + protected function instantiateClass(string $className, array $arguments): object { if (isset($this->classesBeingInstantiated[$className])) { throw new Exception\CannotBuildObjectException('Circular dependency detected while trying to instantiate class "' . $className . '".', 1168505928); diff --git a/Neos.Flow/Classes/Persistence/Doctrine/Query.php b/Neos.Flow/Classes/Persistence/Doctrine/Query.php index 795fd52f9d..06b1b80c67 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/Query.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/Query.php @@ -421,12 +421,12 @@ public function getConstraint() * @return object * @api */ - public function logicalAnd($constraint1) + public function logicalAnd(mixed $constraint1, mixed ...$constraints) { if (is_array($constraint1)) { $constraints = $constraint1; } else { - $constraints = func_get_args(); + $constraints = [$constraint1, ...$constraints]; } return $this->queryBuilder->expr()->andX(...$constraints); } @@ -440,12 +440,12 @@ public function logicalAnd($constraint1) * @return object * @api */ - public function logicalOr($constraint1) + public function logicalOr(mixed $constraint1, mixed ...$constraints) { if (is_array($constraint1)) { $constraints = $constraint1; } else { - $constraints = func_get_args(); + $constraints = [$constraint1, ...$constraints]; } return $this->queryBuilder->expr()->orX(...$constraints); } diff --git a/Neos.Flow/Classes/Persistence/QueryInterface.php b/Neos.Flow/Classes/Persistence/QueryInterface.php index f7a2e70db8..910f37d9d8 100644 --- a/Neos.Flow/Classes/Persistence/QueryInterface.php +++ b/Neos.Flow/Classes/Persistence/QueryInterface.php @@ -223,22 +223,22 @@ public function getConstraint(); * takes one or more constraints and concatenates them with a boolean AND. * It also accepts a single array of constraints to be concatenated. * - * @param mixed ...$constraint1 The first of multiple constraints or an array of constraints. + * @param mixed $constraint1 The first of multiple constraints or an array of constraints. * @return object * @api */ - public function logicalAnd($constraint1); + public function logicalAnd(mixed $constraint1, mixed ...$constraints); /** * Performs a logical disjunction of the two given constraints. The method * takes one or more constraints and concatenates them with a boolean OR. * It also accepts a single array of constraints to be concatenated. * - * @param mixed ...$constraint1 The first of multiple constraints or an array of constraints. + * @param mixed $constraint1 The first of multiple constraints or an array of constraints. * @return object * @api */ - public function logicalOr($constraint1); + public function logicalOr(mixed $constraint1, mixed ...$constraints); /** * Performs a logical negation of the given constraint diff --git a/Neos.Flow/Classes/Property/TypeConverter/ScalarTypeToObjectConverter.php b/Neos.Flow/Classes/Property/TypeConverter/ScalarTypeToObjectConverter.php index 2dcad21ff1..4a3dbc7bf2 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/ScalarTypeToObjectConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/ScalarTypeToObjectConverter.php @@ -71,7 +71,7 @@ public function canConvertFrom($source, $targetType) return false; } $methodParameter = array_shift($methodParameters); - return TypeHandling::normalizeType($methodParameter['type']) === gettype($source); + return TypeHandling::normalizeType($methodParameter['type']) === TypeHandling::normalizeType(gettype($source)); } /** diff --git a/Neos.Flow/Classes/Reflection/ReflectionService.php b/Neos.Flow/Classes/Reflection/ReflectionService.php index 2cde978198..05f63c9fe4 100644 --- a/Neos.Flow/Classes/Reflection/ReflectionService.php +++ b/Neos.Flow/Classes/Reflection/ReflectionService.php @@ -1174,7 +1174,12 @@ public function reflectClassProperty(string $className, PropertyReflection $prop if ($this->isAttributeIgnored($attribute->getName())) { continue; } - $this->classReflectionData[$className][self::DATA_CLASS_PROPERTIES][$propertyName][self::DATA_PROPERTY_ANNOTATIONS][$attribute->getName()][] = $attribute->newInstance(); + try { + $attributeInstance = $attribute->newInstance(); + } catch (\Error $error) { + throw new \RuntimeException(sprintf('Attribute "%s" used in class "%s" was not found.', $attribute->getName(), $className), 1695635128, $error); + } + $this->classReflectionData[$className][self::DATA_CLASS_PROPERTIES][$propertyName][self::DATA_PROPERTY_ANNOTATIONS][$attribute->getName()][] = $attributeInstance; } } diff --git a/Neos.Flow/Configuration/Development/Settings.yaml b/Neos.Flow/Configuration/Development/Settings.yaml index fb918eef38..47095b5141 100644 --- a/Neos.Flow/Configuration/Development/Settings.yaml +++ b/Neos.Flow/Configuration/Development/Settings.yaml @@ -23,6 +23,11 @@ Neos: defaultRenderingOptions: renderTechnicalDetails: true + renderingGroups: + noStacktraceExceptionGroup: + options: + logException: true + errorHandler: exceptionalErrors: ['%E_USER_ERROR%', '%E_RECOVERABLE_ERROR%', '%E_WARNING%', '%E_NOTICE%', '%E_USER_WARNING%', '%E_USER_NOTICE%', '%E_STRICT%'] diff --git a/Neos.Flow/Configuration/Objects.yaml b/Neos.Flow/Configuration/Objects.yaml index be14f55c1e..4f116f465d 100644 --- a/Neos.Flow/Configuration/Objects.yaml +++ b/Neos.Flow/Configuration/Objects.yaml @@ -257,6 +257,9 @@ Neos\Flow\Http\Middleware\MiddlewaresChain: Neos\Flow\Mvc\Routing\RouterInterface: className: Neos\Flow\Mvc\Routing\Router +Neos\Flow\Mvc\Routing\RoutesProviderInterface: + className: Neos\Flow\Mvc\Routing\ConfigurationRoutesProvider + Neos\Flow\Mvc\Routing\RouterCachingService: properties: routeCache: diff --git a/Neos.Flow/Configuration/Settings.Error.yaml b/Neos.Flow/Configuration/Settings.Error.yaml index 5de31e71b7..8f56704c08 100644 --- a/Neos.Flow/Configuration/Settings.Error.yaml +++ b/Neos.Flow/Configuration/Settings.Error.yaml @@ -39,6 +39,14 @@ Neos: variables: errorDescription: 'Sorry, the database connection couldn''t be established.' + noStacktraceExceptionGroup: + matchingExceptionClassNames: ['Neos\Flow\Security\Exception\InvalidHashException'] + options: + logException: false + templatePathAndFilename: 'resource://Neos.Flow/Private/Templates/Error/Default.html' + variables: + errorDescription: 'Sorry, something went wrong.' + errorHandler: # Defines which errors should result in an exception thrown - all other error @@ -73,4 +81,3 @@ Neos: # Maximal recursion for the debugger recursionLimit: 5 - diff --git a/Neos.Flow/Configuration/Settings.Log.yaml b/Neos.Flow/Configuration/Settings.Log.yaml index 236f60b1b5..01b91cc7f9 100644 --- a/Neos.Flow/Configuration/Settings.Log.yaml +++ b/Neos.Flow/Configuration/Settings.Log.yaml @@ -58,7 +58,7 @@ Neos: optionsByImplementation: 'Neos\Flow\Log\ThrowableStorage\FileStorage': storagePath: '%FLOW_PATH_DATA%Logs/Exceptions' - # The maximum age of throwable dump in seconds, 0 to disable cleaning based on age, default 30 days + # The maximum age of throwable dump in seconds, 0 to disable cleaning based on age maximumThrowableDumpAge: 2592000 - # The maximum number of throwable dumps to store, 0 to disable cleaning based on count, default 10.000 + # The maximum number of throwable dumps to store, 0 to disable cleaning based on count maximumThrowableDumpCount: 10000 diff --git a/Neos.Flow/Configuration/Testing/Objects.yaml b/Neos.Flow/Configuration/Testing/Objects.yaml index 35c3348b3b..5ab4af6180 100644 --- a/Neos.Flow/Configuration/Testing/Objects.yaml +++ b/Neos.Flow/Configuration/Testing/Objects.yaml @@ -15,6 +15,11 @@ Neos\Flow\Http\Client\Browser: properties: requestEngine: object: Neos\Flow\Http\Client\InternalRequestEngine +# +# Routing will be extended to be able to add custom routes at runtime +# +Neos\Flow\Mvc\Routing\RoutesProviderInterface: + className: Neos\Flow\Mvc\Routing\TestingRoutesProvider # # Security and PersistentResource handling need specialized testing classes: diff --git a/Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Http.rst b/Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Http.rst index 23da473793..8e3191ae7b 100644 --- a/Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Http.rst +++ b/Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Http.rst @@ -135,8 +135,8 @@ that defines the ``process($request, $next)`` method:: */ final class SomeMiddleware implements MiddlewareInterface { - public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface; - $response = $next->handle($httpRequest); + public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface { + $response = $next->handle($request); return $response->withAddedHeader('X-MyHeader', '123'); } } diff --git a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/AnnotationReference.rst b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/AnnotationReference.rst index 28ff373931..eea55d6e9b 100644 --- a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/AnnotationReference.rst +++ b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/AnnotationReference.rst @@ -3,7 +3,7 @@ Flow Annotation Reference ========================= -This reference was automatically generated from code on 2024-01-25 +This reference was automatically generated from code on 2024-02-23 .. _`Flow Annotation Reference: After`: diff --git a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/CommandReference.rst b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/CommandReference.rst index 7578f4cf76..fe5ebde694 100644 --- a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/CommandReference.rst +++ b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/CommandReference.rst @@ -19,7 +19,7 @@ commands that may be available, use:: ./flow help -The following reference was automatically generated from code on 2024-01-25 +The following reference was automatically generated from code on 2024-02-23 .. _`Flow Command Reference: NEOS.FLOW`: diff --git a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/FluidAdaptorViewHelperReference.rst b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/FluidAdaptorViewHelperReference.rst index cc59fb33da..bdb893a1a0 100644 --- a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/FluidAdaptorViewHelperReference.rst +++ b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/FluidAdaptorViewHelperReference.rst @@ -3,7 +3,7 @@ FluidAdaptor ViewHelper Reference ================================= -This reference was automatically generated from code on 2024-01-25 +This reference was automatically generated from code on 2024-02-23 .. _`FluidAdaptor ViewHelper Reference: f:debug`: diff --git a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/SignalsReference.rst b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/SignalsReference.rst index 2fe50692a7..0edc32f88a 100644 --- a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/SignalsReference.rst +++ b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/SignalsReference.rst @@ -3,7 +3,7 @@ Flow Signals Reference ====================== -This reference was automatically generated from code on 2024-01-25 +This reference was automatically generated from code on 2024-02-23 .. _`Flow Signals Reference: AbstractAdvice (``Neos\Flow\Aop\Advice\AbstractAdvice``)`: diff --git a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/TYPO3FluidViewHelperReference.rst b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/TYPO3FluidViewHelperReference.rst index 296bb1c77d..520f671daf 100644 --- a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/TYPO3FluidViewHelperReference.rst +++ b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/TYPO3FluidViewHelperReference.rst @@ -3,7 +3,7 @@ TYPO3 Fluid ViewHelper Reference ================================ -This reference was automatically generated from code on 2024-01-25 +This reference was automatically generated from code on 2024-02-23 .. _`TYPO3 Fluid ViewHelper Reference: f:alias`: diff --git a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/TypeConverterReference.rst b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/TypeConverterReference.rst index 4893ad8f67..9a325ec210 100644 --- a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/TypeConverterReference.rst +++ b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/TypeConverterReference.rst @@ -3,7 +3,7 @@ Flow TypeConverter Reference ============================ -This reference was automatically generated from code on 2024-01-25 +This reference was automatically generated from code on 2024-02-23 .. _`Flow TypeConverter Reference: ArrayConverter`: diff --git a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/ValidatorReference.rst b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/ValidatorReference.rst index 6d022b9a42..277468c0ae 100644 --- a/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/ValidatorReference.rst +++ b/Neos.Flow/Documentation/TheDefinitiveGuide/PartV/ValidatorReference.rst @@ -3,7 +3,7 @@ Flow Validator Reference ======================== -This reference was automatically generated from code on 2024-01-25 +This reference was automatically generated from code on 2024-02-23 .. _`Flow Validator Reference: AggregateBoundaryValidator`: diff --git a/Neos.Flow/Resources/Public/Error/Exception.css b/Neos.Flow/Resources/Public/Error/Exception.css index bf98d8ab6a..ef760647ee 100644 --- a/Neos.Flow/Resources/Public/Error/Exception.css +++ b/Neos.Flow/Resources/Public/Error/Exception.css @@ -47,6 +47,14 @@ h1, h2, h3 { color: #000000; } +.ExceptionBodyPre { + padding: 10px; + margin: 10px; + color: #000000; + white-space: pre; + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} + .ExceptionProperty { color: #34363C; } diff --git a/Neos.Flow/Tests/Functional/Http/Client/InternalRequestEngineTest.php b/Neos.Flow/Tests/Functional/Http/Client/InternalRequestEngineTest.php index d779fe9949..4af43df7cb 100644 --- a/Neos.Flow/Tests/Functional/Http/Client/InternalRequestEngineTest.php +++ b/Neos.Flow/Tests/Functional/Http/Client/InternalRequestEngineTest.php @@ -11,7 +11,6 @@ * source code. */ -use Neos\Flow\Mvc\Routing\Route; use Neos\Flow\Tests\FunctionalTestCase; /** @@ -31,17 +30,17 @@ protected function setUp(): void { parent::setUp(); - $route = new Route(); - $route->setName('Functional Test - Http::Client::InternalRequestEngine'); - $route->setUriPattern('test/security/restricted'); - $route->setDefaults([ - '@package' => 'Neos.Flow', - '@subpackage' => 'Tests\Functional\Security\Fixtures', - '@controller' => 'Restricted', - '@action' => 'admin', - '@format' => 'html' - ]); - $this->router->addRoute($route); + $this->registerRoute( + 'Functional Test - Http::Client::InternalRequestEngine', + 'test/security/restricted', + [ + '@package' => 'Neos.Flow', + '@subpackage' => 'Tests\Functional\Security\Fixtures', + '@controller' => 'Restricted', + '@action' => 'admin', + '@format' => 'html' + ] + ); } /** diff --git a/Neos.Flow/Tests/Functional/Http/Fixtures/Controller/FooController.php b/Neos.Flow/Tests/Functional/Http/Fixtures/Controller/FooController.php index 35e6dbd723..538371e19a 100644 --- a/Neos.Flow/Tests/Functional/Http/Fixtures/Controller/FooController.php +++ b/Neos.Flow/Tests/Functional/Http/Fixtures/Controller/FooController.php @@ -13,18 +13,19 @@ use Neos\Flow\Mvc\ActionResponse; use Neos\Flow\Mvc\Controller\AbstractController; +use Psr\Http\Message\ResponseInterface; class FooController extends AbstractController { /** * @inheritDoc */ - public function processRequest($request): ActionResponse + public function processRequest($request): ResponseInterface { $response = new ActionResponse(); // test's AbstractController::initializeController $this->initializeController($request, $response); $response->setContent('FooController responded'); - return $response; + return $response->buildHttpResponse(); } } diff --git a/Neos.Flow/Tests/Functional/Http/RequestHandlerTest.php b/Neos.Flow/Tests/Functional/Http/RequestHandlerTest.php index 93a7dfac36..9851abb58f 100644 --- a/Neos.Flow/Tests/Functional/Http/RequestHandlerTest.php +++ b/Neos.Flow/Tests/Functional/Http/RequestHandlerTest.php @@ -11,6 +11,7 @@ * source code. */ +use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Http\RequestHandler; use Neos\Flow\Tests\FunctionalTestCase; use PHPUnit\Framework\MockObject\MockObject; @@ -31,14 +32,11 @@ class RequestHandlerTest extends FunctionalTestCase */ public function httpRequestIsConvertedToAnActionRequestAndDispatchedToTheRespectiveController(): void { - $foundRoute = false; - foreach ($this->router->getRoutes() as $route) { - if ($route->getName() === 'Neos.Flow :: Functional Test: HTTP - FooController') { - $foundRoute = true; - } - } - if (!$foundRoute) { - self::markTestSkipped('In this distribution the Flow routes are not included into the global configuration.'); + if ( + ($this->objectManager->get(ConfigurationManager::class) + ->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Neos.Flow.mvc.routes')['Neos.Flow'] ?? false) !== true + ) { + self::markTestSkipped(sprintf('In this distribution the Flow routes are not included into the global configuration and thus cannot be tested. Please set in Neos.Flow.mvc.routes "Neos.Flow": true.')); } $_SERVER = [ diff --git a/Neos.Flow/Tests/Functional/Mvc/AbstractControllerTest.php b/Neos.Flow/Tests/Functional/Mvc/AbstractControllerTest.php index cf238db8db..facf6d3ef0 100644 --- a/Neos.Flow/Tests/Functional/Mvc/AbstractControllerTest.php +++ b/Neos.Flow/Tests/Functional/Mvc/AbstractControllerTest.php @@ -11,7 +11,6 @@ * source code. */ -use Neos\Flow\Mvc\Routing\Route; use Neos\Flow\Tests\FunctionalTestCase; class AbstractControllerTest extends FunctionalTestCase @@ -27,18 +26,17 @@ class AbstractControllerTest extends FunctionalTestCase protected function setUp(): void { parent::setUp(); - - $route = new Route(); - $route->setName('AbstractControllerTest Route 1'); - $route->setUriPattern('test/mvc/abstractcontrollertesta/{@action}'); - $route->setDefaults([ - '@package' => 'Neos.Flow', - '@subpackage' => 'Tests\Functional\Mvc\Fixtures', - '@controller' => 'AbstractControllerTestA', - '@format' =>'html' - ]); - $route->setAppendExceedingArguments(true); - $this->router->addRoute($route); + $this->registerRoute( + 'AbstractControllerTest Route 1', + 'test/mvc/abstractcontrollertesta/{@action}', + [ + '@package' => 'Neos.Flow', + '@subpackage' => 'Tests\Functional\Mvc\Fixtures', + '@controller' => 'AbstractControllerTestA', + '@format' =>'html' + ], + true + ); } /** diff --git a/Neos.Flow/Tests/Functional/Mvc/RoutingTest.php b/Neos.Flow/Tests/Functional/Mvc/RoutingTest.php index 726aeddce6..814e8a2700 100644 --- a/Neos.Flow/Tests/Functional/Mvc/RoutingTest.php +++ b/Neos.Flow/Tests/Functional/Mvc/RoutingTest.php @@ -12,12 +12,14 @@ */ use GuzzleHttp\Psr7\Uri; +use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; use Neos\Flow\Mvc\Routing\Dto\RouteParameters; use Neos\Flow\Mvc\Routing\Dto\ResolveContext; use Neos\Flow\Mvc\Routing\Dto\RouteContext; use Neos\Flow\Mvc\Routing\Route; +use Neos\Flow\Mvc\Routing\TestingRoutesProvider; use Neos\Flow\Tests\Functional\Mvc\Fixtures\Controller\ActionControllerTestAController; use Neos\Flow\Tests\Functional\Mvc\Fixtures\Controller\RoutingTestAController; use Neos\Flow\Tests\FunctionalTestCase; @@ -46,18 +48,11 @@ protected function setUp(): void parent::setUp(); $this->serverRequestFactory = $this->objectManager->get(ServerRequestFactoryInterface::class); - $foundRoute = false; - /** @var $route Route */ - foreach ($this->router->getRoutes() as $route) { - if ($route->getName() === 'Neos.Flow :: Functional Test: HTTP - FooController') { - $foundRoute = true; - break; - } - } - - if (!$foundRoute) { - self::markTestSkipped('In this distribution the Flow routes are not included into the global configuration.'); - return; + if ( + ($this->objectManager->get(ConfigurationManager::class) + ->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Neos.Flow.mvc.routes')['Neos.Flow'] ?? false) !== true + ) { + self::markTestSkipped(sprintf('In this distribution the Flow routes are not included into the global configuration and thus cannot be tested. Please set in Neos.Flow.mvc.routes "Neos.Flow": true.')); } } @@ -386,7 +381,7 @@ public function uriPathPrefixIsRespectedInRoute() /** * @test */ - public function explicitlySpecifiedRoutesOverruleConfiguredRoutes() + public function testingRoutesProviderCanRegisterOwnRoute() { $routeValues = [ '@package' => 'Neos.Flow', @@ -395,24 +390,20 @@ public function explicitlySpecifiedRoutesOverruleConfiguredRoutes() '@action' => 'index', '@format' => 'html' ]; - $routesConfiguration = [ - [ - 'uriPattern' => 'custom/uri/pattern', - 'defaults' => [ - '@package' => 'Neos.Flow', - '@subpackage' => 'Tests\Functional\Http\Fixtures', - '@controller' => 'Foo', - '@action' => 'index', - '@format' => 'html' - ], - ] - ]; - $this->router->setRoutesConfiguration($routesConfiguration); + + $this->objectManager->get(TestingRoutesProvider::class)->addRoute(Route::fromConfiguration([ + 'uriPattern' => 'custom/uri/pattern', + 'defaults' => [ + '@package' => 'Neos.Flow', + '@subpackage' => 'Tests\Functional\Http\Fixtures', + '@controller' => 'Foo', + '@action' => 'index', + '@format' => 'html' + ], + ])); + $baseUri = new Uri('http://localhost'); $actualResult = $this->router->resolve(new ResolveContext($baseUri, $routeValues, false, '', RouteParameters::createEmpty())); self::assertSame('/custom/uri/pattern', (string)$actualResult); - - // reset router configuration for following tests - $this->router->setRoutesConfiguration(null); } } diff --git a/Neos.Flow/Tests/Functional/Mvc/UriBuilderTest.php b/Neos.Flow/Tests/Functional/Mvc/UriBuilderTest.php index 397a976613..97cbcc1e30 100644 --- a/Neos.Flow/Tests/Functional/Mvc/UriBuilderTest.php +++ b/Neos.Flow/Tests/Functional/Mvc/UriBuilderTest.php @@ -126,7 +126,7 @@ public function whenLinkingToSameHostTheUrlIsAsExpectedNotContainingDoubleSlashe */ public function whenLinkingToRootOfSameHostTheUrlContainsASingleSlash() { - // NOTE: the route part handler here does not really match; as we link to the the route + // NOTE: the route part handler here does not really match; as we link to the route // registered in "registerAbsoluteRoute()". $this->registerSingleRoute(UriBuilderSetDomainRoutePartHandler::class); // NOTE: the registered route is PREPENDED to the existing list; so we need to register the absolute route LAST as it should match FIRST. diff --git a/Neos.Flow/Tests/Functional/Security/AuthenticationTest.php b/Neos.Flow/Tests/Functional/Security/AuthenticationTest.php index 9d283088c4..7b54050948 100644 --- a/Neos.Flow/Tests/Functional/Security/AuthenticationTest.php +++ b/Neos.Flow/Tests/Functional/Security/AuthenticationTest.php @@ -12,7 +12,6 @@ */ use GuzzleHttp\Psr7\Uri; -use Neos\Flow\Mvc\Routing\Route; use Neos\Flow\Security\AccountFactory; use Neos\Flow\Security\AccountRepository; use Neos\Flow\Tests\FunctionalTestCase; @@ -56,57 +55,57 @@ protected function setUp(): void $accountRepository->add($account3); $this->persistenceManager->persistAll(); - $route = new Route(); - $route->setName('Functional Test - Security::Restricted'); - $route->setUriPattern('test/security/restricted(/{@action})'); - $route->setDefaults([ - '@package' => 'Neos.Flow', - '@subpackage' => 'Tests\Functional\Security\Fixtures', - '@controller' => 'Restricted', - '@action' => 'public', - '@format' =>'html' - ]); - $route->setAppendExceedingArguments(true); - $this->router->addRoute($route); - - $route2 = new Route(); - $route2->setName('Functional Test - Security::Authentication'); - $route2->setUriPattern('test/security/authentication(/{@action})'); - $route2->setDefaults([ - '@package' => 'Neos.Flow', - '@subpackage' => 'Tests\Functional\Security\Fixtures', - '@controller' => 'Authentication', - '@action' => 'authenticate', - '@format' => 'html' - ]); - $route2->setAppendExceedingArguments(true); - $this->router->addRoute($route2); - - $route3 = new Route(); - $route3->setName('Functional Test - Security::HttpBasicAuthentication'); - $route3->setUriPattern('test/security/authentication/httpbasic(/{@action})'); - $route3->setDefaults([ - '@package' => 'Neos.Flow', - '@subpackage' => 'Tests\Functional\Security\Fixtures', - '@controller' => 'HttpBasicTest', - '@action' => 'authenticate', - '@format' => 'html' - ]); - $route3->setAppendExceedingArguments(true); - $this->router->addRoute($route3); - - $route4 = new Route(); - $route4->setName('Functional Test - Security::UsernamePasswordAuthentication'); - $route4->setUriPattern('test/security/authentication/usernamepassword(/{@action})'); - $route4->setDefaults([ - '@package' => 'Neos.Flow', - '@subpackage' => 'Tests\Functional\Security\Fixtures', - '@controller' => 'UsernamePasswordTest', - '@action' => 'authenticate', - '@format' => 'html' - ]); - $route4->setAppendExceedingArguments(true); - $this->router->addRoute($route4); + $this->registerRoute( + 'Functional Test - Security::Restricted', + 'test/security/restricted(/{@action})', + [ + '@package' => 'Neos.Flow', + '@subpackage' => 'Tests\Functional\Security\Fixtures', + '@controller' => 'Restricted', + '@action' => 'public', + '@format' =>'html' + ], + true + ); + + $this->registerRoute( + 'Functional Test - Security::Authentication', + 'test/security/authentication(/{@action})', + [ + '@package' => 'Neos.Flow', + '@subpackage' => 'Tests\Functional\Security\Fixtures', + '@controller' => 'Authentication', + '@action' => 'authenticate', + '@format' => 'html' + ], + true + ); + + $this->registerRoute( + 'Functional Test - Security::HttpBasicAuthentication', + 'test/security/authentication/httpbasic(/{@action})', + [ + '@package' => 'Neos.Flow', + '@subpackage' => 'Tests\Functional\Security\Fixtures', + '@controller' => 'HttpBasicTest', + '@action' => 'authenticate', + '@format' => 'html' + ], + true + ); + + $this->registerRoute( + 'Functional Test - Security::UsernamePasswordAuthentication', + 'test/security/authentication/usernamepassword(/{@action})', + [ + '@package' => 'Neos.Flow', + '@subpackage' => 'Tests\Functional\Security\Fixtures', + '@controller' => 'UsernamePasswordTest', + '@action' => 'authenticate', + '@format' => 'html' + ], + true + ); $this->serverRequestFactory = $this->objectManager->get(ServerRequestFactoryInterface::class); } diff --git a/Neos.Flow/Tests/Functional/Session/SessionManagementTest.php b/Neos.Flow/Tests/Functional/Session/SessionManagementTest.php index 726850b1aa..8f309970c7 100644 --- a/Neos.Flow/Tests/Functional/Session/SessionManagementTest.php +++ b/Neos.Flow/Tests/Functional/Session/SessionManagementTest.php @@ -11,7 +11,6 @@ * source code. */ -use Neos\Flow\Mvc\Routing\Route; use Neos\Flow\Tests\FunctionalTestCase; use Neos\Flow\Session; @@ -24,17 +23,17 @@ protected function setUp(): void { parent::setUp(); - $route = new Route(); - $route->setName('Functional Test - Session::SessionTest'); - $route->setUriPattern('test/session(/{@action})'); - $route->setDefaults([ - '@package' => 'Neos.Flow', - '@subpackage' => 'Tests\Functional\Session\Fixtures', - '@controller' => 'SessionTest', - '@action' => 'sessionStart', - '@format' =>'html' - ]); - $this->router->addRoute($route); + $this->registerRoute( + 'Functional Test - Session::SessionTest', + 'test/session(/{@action})', + [ + '@package' => 'Neos.Flow', + '@subpackage' => 'Tests\Functional\Session\Fixtures', + '@controller' => 'SessionTest', + '@action' => 'sessionStart', + '@format' =>'html' + ] + ); } /** diff --git a/Neos.Flow/Tests/FunctionalTestCase.php b/Neos.Flow/Tests/FunctionalTestCase.php index 6af2923449..64b7347b82 100644 --- a/Neos.Flow/Tests/FunctionalTestCase.php +++ b/Neos.Flow/Tests/FunctionalTestCase.php @@ -14,6 +14,7 @@ use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Core\Bootstrap; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Mvc\Routing\TestingRoutesProvider; use Neos\Flow\Security\Authentication\TokenAndProviderFactory; use Neos\Http\Factories\ServerRequestFactory; use Neos\Http\Factories\UriFactory; @@ -263,6 +264,7 @@ protected function tearDown(): void } self::$bootstrap->getObjectManager()->forgetInstance(\Neos\Flow\Http\Client\InternalRequestEngine::class); + self::$bootstrap->getObjectManager()->get(TestingRoutesProvider::class)->reset(); self::$bootstrap->getObjectManager()->forgetInstance(\Neos\Flow\Persistence\Aspect\PersistenceMagicAspect::class); $this->inject(self::$bootstrap->getObjectManager()->get(\Neos\Flow\ResourceManagement\ResourceRepository::class), 'addedResources', new \SplObjectStorage()); $this->inject(self::$bootstrap->getObjectManager()->get(\Neos\Flow\ResourceManagement\ResourceRepository::class), 'removedResources', new \SplObjectStorage()); @@ -369,7 +371,10 @@ protected function registerRoute($name, $uriPattern, array $defaults, $appendExc if ($httpMethods !== null) { $route->setHttpMethods($httpMethods); } - $this->router->addRoute($route); + + $testingRoutesProvider = $this->objectManager->get(TestingRoutesProvider::class); + $testingRoutesProvider->addRoute($route); + return $route; } @@ -430,7 +435,6 @@ protected function setupHttp() $this->browser = new \Neos\Flow\Http\Client\Browser(); $this->browser->setRequestEngine(new \Neos\Flow\Http\Client\InternalRequestEngine()); $this->router = $this->browser->getRequestEngine()->getRouter(); - $this->router->setRoutesConfiguration(null); $serverRequestFactory = new ServerRequestFactory(new UriFactory()); $request = $serverRequestFactory->createServerRequest('GET', 'http://localhost/neos/flow/test'); diff --git a/Neos.Flow/Tests/Unit/Cache/CacheFactoryTest.php b/Neos.Flow/Tests/Unit/Cache/CacheFactoryTest.php index e918d8bbe4..62eb9617d8 100644 --- a/Neos.Flow/Tests/Unit/Cache/CacheFactoryTest.php +++ b/Neos.Flow/Tests/Unit/Cache/CacheFactoryTest.php @@ -99,9 +99,7 @@ public function createInjectsAnInstanceOfTheSpecifiedBackendIntoTheCacheFrontend */ public function aDifferentDefaultCacheDirectoryIsUsedForPersistentFileCaches() { - $cacheManager = new CacheManager(); $factory = new CacheFactory(new ApplicationContext('Testing'), $this->mockEnvironment, 'UnitTesting'); - $factory->injectCacheManager($cacheManager); $factory->injectEnvironmentConfiguration($this->mockEnvironmentConfiguration); $cache = $factory->create('Persistent_Cache', VariableFrontend::class, FileBackend::class, [], true); diff --git a/Neos.Flow/Tests/Unit/Fixtures/ClassWithFloatConstructor.php b/Neos.Flow/Tests/Unit/Fixtures/ClassWithFloatConstructor.php new file mode 100644 index 0000000000..72aa870a1a --- /dev/null +++ b/Neos.Flow/Tests/Unit/Fixtures/ClassWithFloatConstructor.php @@ -0,0 +1,33 @@ +value = $value; + } +} diff --git a/Neos.Flow/Tests/Unit/Fixtures/ClassWithIntegerConstructor.php b/Neos.Flow/Tests/Unit/Fixtures/ClassWithIntegerConstructor.php index 5f7ef298bb..1635e7f9fa 100644 --- a/Neos.Flow/Tests/Unit/Fixtures/ClassWithIntegerConstructor.php +++ b/Neos.Flow/Tests/Unit/Fixtures/ClassWithIntegerConstructor.php @@ -17,7 +17,7 @@ class ClassWithIntegerConstructor { /** - * @var string + * @var int */ public $value; diff --git a/Neos.Flow/Tests/Unit/Mvc/Controller/AbstractControllerTest.php b/Neos.Flow/Tests/Unit/Mvc/Controller/AbstractControllerTest.php index b8b994c207..a931d904d7 100644 --- a/Neos.Flow/Tests/Unit/Mvc/Controller/AbstractControllerTest.php +++ b/Neos.Flow/Tests/Unit/Mvc/Controller/AbstractControllerTest.php @@ -14,6 +14,7 @@ use GuzzleHttp\Psr7\ServerRequest; use GuzzleHttp\Psr7\Uri; use PHPUnit\Framework\Assert; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\ActionResponse; @@ -291,7 +292,7 @@ public function redirectRedirectsToTheSpecifiedAction() $mockUriBuilder->expects(self::once())->method('uriFor')->with('show', ['foo' => 'bar'], 'Stuff', 'Super', 'Duper\Package')->willReturn('the_uri'); $controller = new class extends AbstractController { - public function processRequest(ActionRequest $request): ActionResponse + public function processRequest(ActionRequest $request): ResponseInterface { $response = new ActionResponse(); $mockUriBuilder = $this->uriBuilder; @@ -300,7 +301,7 @@ public function processRequest(ActionRequest $request): ActionResponse $this->myIndexAction(); - return $this->response; + return $this->response->buildHttpResponse(); } public function myIndexAction(): void @@ -315,7 +316,7 @@ public function myIndexAction(): void $controller->processRequest($this->mockActionRequest); } catch (StopActionException $exception) { $actionResponse = $exception->response; - Assert::assertSame('the_uri', $actionResponse->getRedirectUri()?->__toString()); + Assert::assertSame('the_uri', $actionResponse->getHeaderLine('Location')); Assert::assertSame(303, $actionResponse->getStatusCode()); return; } @@ -336,7 +337,7 @@ public function redirectUsesRequestFormatAsDefaultAndUnsetsSubPackageKeyIfNecess $mockUriBuilder->expects(self::once())->method('uriFor')->with('show', ['foo' => 'bar'], 'Stuff', 'Super', null)->willReturn('the_uri'); $controller = new class extends AbstractController { - public function processRequest(ActionRequest $request): ActionResponse + public function processRequest(ActionRequest $request): ResponseInterface { $response = new ActionResponse(); $mockUriBuilder = $this->uriBuilder; @@ -345,7 +346,7 @@ public function processRequest(ActionRequest $request): ActionResponse $this->myIndexAction(); - return $this->response; + return $this->response->buildHttpResponse(); } public function myIndexAction(): void @@ -360,7 +361,7 @@ public function myIndexAction(): void $controller->processRequest($this->mockActionRequest); } catch (StopActionException $exception) { $actionResponse = $exception->response; - Assert::assertSame('the_uri', $actionResponse->getRedirectUri()?->__toString()); + Assert::assertSame('the_uri', $actionResponse->getHeaderLine('Location')); Assert::assertSame(303, $actionResponse->getStatusCode()); return; } @@ -419,7 +420,7 @@ public function redirectToUriSetsRedirectUri() } self::assertNotNull($response); - self::assertSame($uri, (string)$response->getRedirectUri()); + self::assertSame($uri, $response->getHeaderLine('Location')); } /** @@ -465,10 +466,12 @@ public function throwStatusSetsTheSpecifiedStatusHeaderAndStopsTheCurrentAction( try { $controller->_call('throwStatus', 404, 'File Really Not Found', $message); } catch (StopActionException $e) { + self::assertSame(404, $e->response->getStatusCode()); + self::assertSame($message, $e->response->getBody()->getContents()); + return; } - self::assertSame(404, $this->actionResponse->getStatusCode()); - self::assertSame($message, $this->actionResponse->getContent()); + self::fail('Expected throwStatus to throw.'); } /** @@ -482,10 +485,12 @@ public function throwStatusSetsTheStatusMessageAsContentIfNoFurtherContentIsProv try { $controller->_call('throwStatus', 404); } catch (StopActionException $e) { + self::assertSame(404, $e->response->getStatusCode()); + self::assertSame('404 Not Found', $e->response->getBody()->getContents()); + return; } - self::assertSame(404, $this->actionResponse->getStatusCode()); - self::assertSame('404 Not Found', $this->actionResponse->getContent()); + self::fail('Expected throwStatus to throw.'); } /** @@ -505,12 +510,11 @@ public function mapRequestArgumentsToControllerArgumentsDoesJustThat() } $controller = $this->getAccessibleMock(AbstractController::class, ['processRequest']); - $controller->_set('arguments', $controllerArguments); $this->mockActionRequest->expects(self::atLeast(2))->method('hasArgument')->withConsecutive(['foo'], ['baz'])->willReturn(true); $this->mockActionRequest->expects(self::atLeast(2))->method('getArgument')->withConsecutive(['foo'], ['baz'])->willReturnOnConsecutiveCalls('bar', 'quux'); - $controller->_call('mapRequestArgumentsToControllerArguments', $this->mockActionRequest); + $controller->_call('mapRequestArgumentsToControllerArguments', $this->mockActionRequest, $controllerArguments); self::assertEquals('bar', $controllerArguments['foo']->getValue()); self::assertEquals('quux', $controllerArguments['baz']->getValue()); } @@ -533,11 +537,10 @@ public function mapRequestArgumentsToControllerArgumentsThrowsExceptionIfRequire } $controller = $this->getAccessibleMock(AbstractController::class, ['processRequest']); - $controller->_set('arguments', $controllerArguments); $this->mockActionRequest->expects(self::exactly(2))->method('hasArgument')->withConsecutive(['foo'], ['baz'])->willReturnOnConsecutiveCalls(true, false); $this->mockActionRequest->expects(self::once())->method('getArgument')->with('foo')->willReturn('bar'); - $controller->_call('mapRequestArgumentsToControllerArguments', $this->mockActionRequest); + $controller->_call('mapRequestArgumentsToControllerArguments', $this->mockActionRequest, $controllerArguments); } } diff --git a/Neos.Flow/Tests/Unit/Mvc/Controller/ActionControllerTest.php b/Neos.Flow/Tests/Unit/Mvc/Controller/ActionControllerTest.php index 16a4378948..991b33d9a8 100644 --- a/Neos.Flow/Tests/Unit/Mvc/Controller/ActionControllerTest.php +++ b/Neos.Flow/Tests/Unit/Mvc/Controller/ActionControllerTest.php @@ -219,13 +219,12 @@ public function processRequestInjectsControllerContextToView() $this->mockRequest->expects(self::any())->method('getHttpRequest')->will(self::returnValue($mockHttpRequest)); $mockResponse = new Mvc\ActionResponse; - $mockResponse->setContentType('text/plain'); $this->inject($this->actionController, 'response', $mockResponse); $mockView = $this->createMock(Mvc\View\ViewInterface::class); $mockView->expects(self::once())->method('setControllerContext')->with($this->mockControllerContext); $this->actionController->expects(self::once())->method('resolveView')->with($this->mockRequest)->will(self::returnValue($mockView)); - $this->actionController->expects(self::once())->method('callActionMethod')->willReturn($mockResponse); + $this->actionController->expects(self::once())->method('callActionMethod')->willReturn(new Response()); $this->actionController->expects(self::once())->method('resolveActionMethodName')->with($this->mockRequest)->will(self::returnValue('someAction')); $this->actionController->processRequest($this->mockRequest); @@ -251,11 +250,10 @@ public function processRequestInjectsSettingsToView() $mockHttpRequest = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); $this->mockRequest->expects(self::any())->method('getHttpRequest')->will(self::returnValue($mockHttpRequest)); - $mockResponse = new Mvc\ActionResponse(); $mockView = $this->createMock(Mvc\View\ViewInterface::class); $mockView->expects(self::once())->method('assign')->with('settings', $mockSettings); $this->actionController->expects(self::once())->method('resolveView')->with($this->mockRequest)->will(self::returnValue($mockView)); - $this->actionController->expects(self::once())->method('callActionMethod')->willReturn($mockResponse); + $this->actionController->expects(self::once())->method('callActionMethod')->willReturn(new Response()); $this->actionController->expects(self::once())->method('resolveActionMethodName')->with($this->mockRequest)->will(self::returnValue('someAction')); $this->actionController->processRequest($this->mockRequest); } @@ -289,12 +287,11 @@ public function processRequestSetsNegotiatedContentTypeOnResponse($supportedMedi $mockHttpRequest->method('getHeaderLine')->with('Accept')->willReturn($acceptHeader); $this->mockRequest->method('getHttpRequest')->willReturn($mockHttpRequest); - $mockResponse = new Mvc\ActionResponse; - $this->actionController->expects(self::once())->method('callActionMethod')->willReturn($mockResponse); + $this->actionController->expects(self::once())->method('callActionMethod')->willReturn(new Response()); $this->inject($this->actionController, 'supportedMediaTypes', $supportedMediaTypes); $response = $this->actionController->processRequest($this->mockRequest); - self::assertSame($expected, $response->getContentType()); + self::assertSame($expected, $response->getHeaderLine('Content-Type')); } /** @@ -307,8 +304,8 @@ public function processRequestUsesContentTypeFromActionResponse($supportedMediaT $this->actionController->method('resolveActionMethodName')->willReturn('indexAction'); $this->inject($this->actionController, 'objectManager', $this->mockObjectManager); - $mockResponse = new Mvc\ActionResponse; - $mockResponse->setContentType('application/json'); + $mockResponse = new Response(); + $mockResponse = $mockResponse->withHeader('Content-Type', 'application/json'); $this->inject($this->actionController, 'supportedMediaTypes', ['application/xml']); $this->actionController->expects(self::once())->method('callActionMethod')->willReturn($mockResponse); @@ -321,10 +318,8 @@ public function processRequestUsesContentTypeFromActionResponse($supportedMediaT $mockHttpRequest->method('getHeaderLine')->with('Accept')->willReturn('application/xml'); $this->mockRequest->method('getHttpRequest')->willReturn($mockHttpRequest); - - $response = $this->actionController->processRequest($this->mockRequest); - self::assertSame('application/json', $response->getContentType()); + self::assertSame('application/json', $response->getHeaderLine('Content-Type')); } /** @@ -347,8 +342,6 @@ public function processRequestUsesContentTypeFromRenderedView($supportedMediaTyp $mockHttpRequest->method('getHeaderLine')->with('Accept')->willReturn('application/xml'); $this->mockRequest->method('getHttpRequest')->willReturn($mockHttpRequest); - $mockResponse = new Mvc\ActionResponse; - $this->inject($this->actionController, 'supportedMediaTypes', ['application/xml']); $mockView = $this->createMock(Mvc\View\ViewInterface::class); @@ -356,7 +349,7 @@ public function processRequestUsesContentTypeFromRenderedView($supportedMediaTyp $this->actionController->expects(self::once())->method('resolveView')->with($this->mockRequest)->willReturn($mockView); $mockResponse = $this->actionController->processRequest($this->mockRequest); - self::assertSame('application/json', $mockResponse->getContentType()); + self::assertSame('application/json', $mockResponse->getHeaderLine('Content-Type')); } /** @@ -408,7 +401,6 @@ public function initializeActionMethodValidatorsDoesNotAddValidatorForIgnoredArg $this->actionController->expects(self::any())->method('getInformationNeededForInitializeActionMethodValidators')->will(self::returnValue([[], [], [], $ignoredValidationArguments])); $this->inject($this->actionController, 'actionMethodName', 'showAction'); - $this->inject($this->actionController, 'arguments', $arguments); $this->inject($this->actionController, 'objectManager', $this->mockObjectManager); @@ -423,6 +415,6 @@ public function initializeActionMethodValidatorsDoesNotAddValidatorForIgnoredArg $mockArgument->expects(self::never())->method('setValidator'); } - $this->actionController->_call('initializeActionMethodValidators'); + $this->actionController->_call('initializeActionMethodValidators', $arguments); } } diff --git a/Neos.Flow/Tests/Unit/Mvc/DispatchMiddlewareTest.php b/Neos.Flow/Tests/Unit/Mvc/DispatchMiddlewareTest.php index 2000dcc3dc..3e89af1bc1 100644 --- a/Neos.Flow/Tests/Unit/Mvc/DispatchMiddlewareTest.php +++ b/Neos.Flow/Tests/Unit/Mvc/DispatchMiddlewareTest.php @@ -14,7 +14,6 @@ use GuzzleHttp\Psr7\Response; use Neos\Flow\Http\ServerRequestAttributes; use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\ActionResponse; use Neos\Flow\Mvc\DispatchMiddleware; use Neos\Flow\Mvc\Dispatcher; use Neos\Flow\Tests\UnitTestCase; @@ -81,8 +80,8 @@ public function processDispatchesTheRequest() { $testContentType = 'audio/ogg'; $this->mockHttpRequest->method('getQueryParams')->willReturn([]); - $testResponse = new ActionResponse(); - $testResponse->setContentType($testContentType); + $testResponse = new Response(); + $testResponse = $testResponse->withHeader('Content-Type', $testContentType); $this->mockDispatcher->expects(self::once())->method('dispatch')->with($this->mockActionRequest)->willReturn($testResponse); $response = $this->dispatchMiddleware->process($this->mockHttpRequest, $this->mockRequestHandler); diff --git a/Neos.Flow/Tests/Unit/Mvc/DispatcherTest.php b/Neos.Flow/Tests/Unit/Mvc/DispatcherTest.php index 4e942bf4ac..38519ea29c 100644 --- a/Neos.Flow/Tests/Unit/Mvc/DispatcherTest.php +++ b/Neos.Flow/Tests/Unit/Mvc/DispatcherTest.php @@ -11,6 +11,7 @@ * source code. */ +use GuzzleHttp\Psr7\Response; use Neos\Flow\Log\PsrLoggerFactoryInterface; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\ActionResponse; @@ -142,7 +143,7 @@ public function dispatchCallsTheControllersProcessRequestMethodUntilTheIsDispatc { $this->mockActionRequest->expects(self::exactly(3))->method('isDispatched')->willReturnOnConsecutiveCalls(false, false, true); - $this->mockController->expects(self::exactly(2))->method('processRequest')->with($this->mockActionRequest)->willReturn($this->actionResponse); + $this->mockController->expects(self::exactly(2))->method('processRequest')->with($this->mockActionRequest)->willReturn(new Response()); $this->dispatcher->dispatch($this->mockActionRequest); } @@ -155,7 +156,7 @@ public function dispatchIgnoresStopExceptionsForFirstLevelActionRequests() $this->mockParentRequest->expects(self::exactly(2))->method('isDispatched')->willReturnOnConsecutiveCalls(false, true); $this->mockParentRequest->expects(self::once())->method('isMainRequest')->willReturn(true); - $this->mockController->expects(self::atLeastOnce())->method('processRequest')->will(self::throwException(StopActionException::createForResponse(new ActionResponse(), ''))); + $this->mockController->expects(self::atLeastOnce())->method('processRequest')->will(self::throwException(StopActionException::createForResponse(new Response(), ''))); $this->dispatcher->dispatch($this->mockParentRequest); } @@ -168,7 +169,7 @@ public function dispatchCatchesStopExceptionOfActionRequestsAndRollsBackToThePar $this->mockActionRequest->expects(self::atLeastOnce())->method('isDispatched')->willReturn(false); $this->mockParentRequest->expects(self::atLeastOnce())->method('isDispatched')->willReturn(true); - $this->mockController->expects(self::atLeastOnce())->method('processRequest')->will(self::throwException(StopActionException::createForResponse(new ActionResponse(), ''))); + $this->mockController->expects(self::atLeastOnce())->method('processRequest')->will(self::throwException(StopActionException::createForResponse(new Response(), ''))); $this->dispatcher->dispatch($this->mockActionRequest, $this->actionResponse); } @@ -187,7 +188,7 @@ public function dispatchContinuesWithNextRequestFoundInAForwardException() $this->mockController->expects(self::exactly(2))->method('processRequest') ->withConsecutive([$this->mockActionRequest], [$this->mockParentRequest]) - ->willReturnOnConsecutiveCalls(self::throwException(StopActionException::createForResponse(new ActionResponse(), '')), self::throwException($forwardException)); + ->willReturnOnConsecutiveCalls(self::throwException(StopActionException::createForResponse(new Response(), '')), self::throwException($forwardException)); $this->dispatcher->dispatch($this->mockActionRequest); } @@ -197,8 +198,7 @@ public function dispatchContinuesWithNextRequestFoundInAForwardException() */ public function dispatchThrowsAnInfiniteLoopExceptionIfTheRequestCouldNotBeDispachedAfter99Iterations() { - $response = new ActionResponse(); - $this->mockController->expects(self::any())->method('processRequest')->with($this->mockActionRequest)->willReturn($response); + $this->mockController->expects(self::any())->method('processRequest')->with($this->mockActionRequest)->willReturn(new Response()); $this->expectException(InfiniteLoopException::class); $requestCallCounter = 0; diff --git a/Neos.Flow/Tests/Unit/Mvc/Routing/ConfigurationRoutesProviderTest.php b/Neos.Flow/Tests/Unit/Mvc/Routing/ConfigurationRoutesProviderTest.php new file mode 100644 index 0000000000..08651da9ad --- /dev/null +++ b/Neos.Flow/Tests/Unit/Mvc/Routing/ConfigurationRoutesProviderTest.php @@ -0,0 +1,77 @@ +createMock(ConfigurationManager::class); + $mockConfigurationManager->expects($this->never())->method('getConfiguration'); + $configurationRoutesProvider = new Routing\ConfigurationRoutesProvider($mockConfigurationManager); + $this->assertInstanceOf(Routing\ConfigurationRoutesProvider::class, $configurationRoutesProvider); + } + + /** + * @test + */ + public function configurationFomConfigurationManagerIsHandled(): void + { + $configuration = [ + [ + 'name' => 'Route 1', + 'uriPattern' => 'route1/{@package}/{@controller}/{@action}(.{@format})', + 'defaults' => ['@format' => 'html'] + ], + [ + 'name' => 'Route 2', + 'uriPattern' => 'route2/{@package}/{@controller}/{@action}(.{@format})', + 'defaults' => ['@format' => 'html'], + 'appendExceedingArguments' => true, + 'cache' => ['lifetime' => 10000, 'tags' => ['foo', 'bar']] + ], + ]; + + $mockConfigurationManager = $this->createMock(ConfigurationManager::class); + $mockConfigurationManager->expects($this->once())->method('getConfiguration')->with(ConfigurationManager::CONFIGURATION_TYPE_ROUTES)->willReturn($configuration); + + $expectedRoute1 = new Route(); + $expectedRoute1->setName('Route 1'); + $expectedRoute1->setUriPattern('route1/{@package}/{@controller}/{@action}(.{@format})'); + $expectedRoute1->setDefaults(['@format' => 'html']); + + $expectedRoute2 = new Route(); + $expectedRoute2->setName('Route 2'); + $expectedRoute2->setUriPattern('route2/{@package}/{@controller}/{@action}(.{@format})'); + $expectedRoute2->setDefaults(['@format' => 'html']); + $expectedRoute2->setCacheLifetime(Routing\Dto\RouteLifetime::fromInt(10000)); + $expectedRoute2->setCacheTags(Routing\Dto\RouteTags::createFromArray(['foo', 'bar'])); + $expectedRoute2->setAppendExceedingArguments(true); + + $expectedRoutes = Routes::create($expectedRoute1, $expectedRoute2); + + $configurationRoutesProvider = new Routing\ConfigurationRoutesProvider($mockConfigurationManager); + $this->assertEquals($expectedRoutes, $configurationRoutesProvider->getRoutes()); + } +} diff --git a/Neos.Flow/Tests/Unit/Mvc/Routing/RouterTest.php b/Neos.Flow/Tests/Unit/Mvc/Routing/RouterTest.php index 3102cc8247..a793bf623b 100644 --- a/Neos.Flow/Tests/Unit/Mvc/Routing/RouterTest.php +++ b/Neos.Flow/Tests/Unit/Mvc/Routing/RouterTest.php @@ -11,9 +11,6 @@ * source code. */ -use GuzzleHttp\Psr7\Uri; -use Neos\Flow\Configuration\ConfigurationManager; -use Neos\Flow\Mvc\Exception\InvalidRouteSetupException; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; use Neos\Flow\Mvc\Routing\Dto\RouteLifetime; use Neos\Flow\Mvc\Routing\Dto\RouteParameters; @@ -25,6 +22,8 @@ use Neos\Flow\Mvc\Routing\Router; use Neos\Flow\Mvc\Routing\RouterCachingService; use Neos\Flow\Mvc\ActionRequest; +use Neos\Flow\Mvc\Routing\Routes; +use Neos\Flow\Mvc\Routing\RoutesProviderInterface; use Neos\Flow\Tests\UnitTestCase; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UriInterface; @@ -73,6 +72,10 @@ protected function setUp(): void { $this->router = $this->getAccessibleMock(Router::class, ['dummy']); + $mockRoutesProvider = $this->createMock(RoutesProviderInterface::class); + $mockRoutesProvider->method('getRoutes')->willReturn(Routes::empty()); + $this->inject($this->router, 'routesProvider', $mockRoutesProvider); + $this->mockSystemLogger = $this->createMock(LoggerInterface::class); $this->inject($this->router, 'logger', $this->mockSystemLogger); @@ -99,101 +102,13 @@ protected function setUp(): void $this->mockActionRequest = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock(); } - /** - * @test - */ - public function resolveCallsCreateRoutesFromConfiguration() - { - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['createRoutesFromConfiguration']); - $this->inject($router, 'routerCachingService', $this->mockRouterCachingService); - $this->inject($router, 'logger', $this->mockSystemLogger); - - // not saying anything, but seems better than to expect the exception we'd get otherwise - /** @var Route|\PHPUnit\Framework\MockObject\MockObject $mockRoute */ - $mockRoute = $this->createMock(Route::class); - $mockRoute->expects(self::once())->method('resolves')->willReturn(true); - $mockRoute->expects(self::atLeastOnce())->method('getResolvedUriConstraints')->willReturn(UriConstraints::create()); - - $this->inject($router, 'routes', [$mockRoute]); - - // this we actually want to know - $router->expects(self::once())->method('createRoutesFromConfiguration'); - $router->resolve(new ResolveContext($this->mockBaseUri, [], false, '', RouteParameters::createEmpty())); - } - - /** - * @test - */ - public function createRoutesFromConfigurationParsesTheGivenConfigurationAndBuildsRouteObjectsFromIt() - { - $routesConfiguration = []; - $routesConfiguration['route1']['uriPattern'] = 'number1'; - $routesConfiguration['route2']['uriPattern'] = 'number2'; - $routesConfiguration['route3'] = [ - 'name' => 'route3', - 'defaults' => ['foodefault'], - 'routeParts' => ['fooroutepart'], - 'uriPattern' => 'number3', - 'toLowerCase' => false, - 'appendExceedingArguments' => true, - 'httpMethods' => ['POST', 'PUT'] - ]; - - $this->router->setRoutesConfiguration($routesConfiguration); - $this->router->_call('createRoutesFromConfiguration'); - - /** @var Route[] $createdRoutes */ - $createdRoutes = $this->router->_get('routes'); - - self::assertEquals('number1', $createdRoutes[0]->getUriPattern()); - self::assertTrue($createdRoutes[0]->isLowerCase()); - self::assertFalse($createdRoutes[0]->getAppendExceedingArguments()); - self::assertEquals('number2', $createdRoutes[1]->getUriPattern()); - self::assertFalse($createdRoutes[1]->hasHttpMethodConstraints()); - self::assertEquals([], $createdRoutes[1]->getHttpMethods()); - self::assertEquals('route3', $createdRoutes[2]->getName()); - self::assertEquals(['foodefault'], $createdRoutes[2]->getDefaults()); - self::assertEquals(['fooroutepart'], $createdRoutes[2]->getRoutePartsConfiguration()); - self::assertEquals('number3', $createdRoutes[2]->getUriPattern()); - self::assertFalse($createdRoutes[2]->isLowerCase()); - self::assertTrue($createdRoutes[2]->getAppendExceedingArguments()); - self::assertTrue($createdRoutes[2]->hasHttpMethodConstraints()); - self::assertEquals(['POST', 'PUT'], $createdRoutes[2]->getHttpMethods()); - } - - /** - * @test - */ - public function createRoutesFromConfigurationThrowsExceptionIfOnlySomeRoutesWithTheSameUriPatternHaveHttpMethodConstraints() - { - $this->expectException(InvalidRouteSetupException::class); - $routesConfiguration = [ - [ - 'uriPattern' => 'somePattern' - ], - [ - 'uriPattern' => 'somePattern', - 'httpMethods' => ['POST', 'PUT'] - ], - ]; - shuffle($routesConfiguration); - $this->router->setRoutesConfiguration($routesConfiguration); - $this->router->_call('createRoutesFromConfiguration'); - } - /** * @test */ public function resolveIteratesOverTheRegisteredRoutesAndReturnsTheResolvedUriConstraintsIfAny() { - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['createRoutesFromConfiguration']); - $this->inject($router, 'routerCachingService', $this->mockRouterCachingService); - $this->inject($router, 'logger', $this->mockSystemLogger); $routeValues = ['foo' => 'bar']; $resolveContext = new ResolveContext($this->mockBaseUri, $routeValues, false, '', RouteParameters::createEmpty()); - $route1 = $this->getMockBuilder(Route::class)->disableOriginalConstructor()->setMethods(['resolves'])->getMock(); $route1->expects(self::once())->method('resolves')->with($resolveContext)->willReturn(false); @@ -204,12 +119,12 @@ public function resolveIteratesOverTheRegisteredRoutesAndReturnsTheResolvedUriCo $route3 = $this->getMockBuilder(Route::class)->disableOriginalConstructor()->setMethods(['resolves'])->getMock(); $route3->expects(self::never())->method('resolves'); - $mockRoutes = [$route1, $route2, $route3]; + $mockRoutesProvider = $this->createMock(RoutesProviderInterface::class); + $mockRoutesProvider->expects($this->once())->method("getRoutes")->willReturn(Routes::create($route1, $route2, $route3)); + $this->inject($this->router, 'routesProvider', $mockRoutesProvider); - $router->expects(self::once())->method('createRoutesFromConfiguration'); - $router->_set('routes', $mockRoutes); + $resolvedUri = $this->router->resolve($resolveContext); - $resolvedUri = $router->resolve($resolveContext); self::assertSame('/route2', $resolvedUri->getPath()); } @@ -219,10 +134,6 @@ public function resolveIteratesOverTheRegisteredRoutesAndReturnsTheResolvedUriCo public function resolveThrowsExceptionIfNoMatchingRouteWasFound() { $this->expectException(NoMatchingRouteException::class); - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['createRoutesFromConfiguration']); - $this->inject($router, 'routerCachingService', $this->mockRouterCachingService); - $this->inject($router, 'logger', $this->mockSystemLogger); $route1 = $this->createMock(Route::class); $route1->expects(self::once())->method('resolves')->willReturn(false); @@ -230,11 +141,11 @@ public function resolveThrowsExceptionIfNoMatchingRouteWasFound() $route2 = $this->createMock(Route::class); $route2->expects(self::once())->method('resolves')->willReturn(false); - $mockRoutes = [$route1, $route2]; - - $router->_set('routes', $mockRoutes); + $mockRoutesProvider = $this->createMock(RoutesProviderInterface::class); + $mockRoutesProvider->expects($this->once())->method("getRoutes")->willReturn(Routes::create($route1, $route2)); + $this->inject($this->router, 'routesProvider', $mockRoutesProvider); - $router->resolve(new ResolveContext($this->mockBaseUri, [], false, '', RouteParameters::createEmpty())); + $this->router->resolve(new ResolveContext($this->mockBaseUri, [], false, '', RouteParameters::createEmpty())); } /** @@ -250,12 +161,6 @@ public function getLastResolvedRouteReturnsNullByDefault() */ public function resolveSetsLastResolvedRoute() { - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['createRoutesFromConfiguration']); - $this->inject($router, 'routerCachingService', $this->mockRouterCachingService); - $this->inject($router, 'logger', $this->mockSystemLogger); - - $routeValues = ['some' => 'route values']; $resolveContext = new ResolveContext($this->mockBaseUri, $routeValues, false, '', RouteParameters::createEmpty()); $mockRoute1 = $this->getMockBuilder(Route::class)->getMock(); @@ -264,11 +169,13 @@ public function resolveSetsLastResolvedRoute() $mockRoute2->expects(self::once())->method('resolves')->with($resolveContext)->willReturn(true); $mockRoute2->method('getResolvedUriConstraints')->willReturn(UriConstraints::create()); - $router->_set('routes', [$mockRoute1, $mockRoute2]); + $mockRoutesProvider = $this->createMock(RoutesProviderInterface::class); + $mockRoutesProvider->expects($this->once())->method("getRoutes")->willReturn(Routes::create($mockRoute1, $mockRoute2)); + $this->inject($this->router, 'routesProvider', $mockRoutesProvider); - $router->resolve($resolveContext); + $this->router->resolve($resolveContext); - self::assertSame($mockRoute2, $router->getLastResolvedRoute()); + self::assertSame($mockRoute2, $this->router->getLastResolvedRoute()); } /** @@ -276,22 +183,20 @@ public function resolveSetsLastResolvedRoute() */ public function resolveReturnsCachedResolvedUriIfFoundInCache() { - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['createRoutesFromConfiguration']); - $this->inject($router, 'routerCachingService', $this->mockRouterCachingService); - $this->inject($router, 'logger', $this->mockSystemLogger); - $routeValues = ['some' => 'route values']; $mockCachedResolvedUriConstraints = UriConstraints::create()->withPath('cached/path'); $resolveContext = new ResolveContext($this->mockBaseUri, $routeValues, false, '', RouteParameters::createEmpty()); - $mockRouterCachingService = $this->getMockBuilder(RouterCachingService::class)->getMock(); - $mockRouterCachingService->method('getCachedResolvedUriConstraints')->with($resolveContext)->willReturn($mockCachedResolvedUriConstraints); - $router->_set('routerCachingService', $mockRouterCachingService); + $mockRouterCachingService = $this->createMock(RouterCachingService::class); + $mockRouterCachingService->expects($this->once())->method('getCachedResolvedUriConstraints')->with($resolveContext)->willReturn($mockCachedResolvedUriConstraints); + $this->inject($this->router, 'routerCachingService', $mockRouterCachingService); - $router->expects(self::never())->method('createRoutesFromConfiguration'); - self::assertSame('/cached/path', (string)$router->resolve($resolveContext)); + $mockRoutesProvider = $this->createMock(RoutesProviderInterface::class); + $mockRoutesProvider->expects($this->never())->method("getRoutes"); + $this->inject($this->router, 'routesProvider', $mockRoutesProvider); + + self::assertSame('/cached/path', (string)$this->router->resolve($resolveContext)); } /** @@ -299,11 +204,6 @@ public function resolveReturnsCachedResolvedUriIfFoundInCache() */ public function resolveStoresResolvedUriPathInCacheIfNotFoundInCache() { - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['createRoutesFromConfiguration']); - $this->inject($router, 'routerCachingService', $this->mockRouterCachingService); - $this->inject($router, 'logger', $this->mockSystemLogger); - $routeValues = ['some' => 'route values']; $mockResolvedUriConstraints = UriConstraints::create()->withPath('resolved/path'); @@ -315,10 +215,16 @@ public function resolveStoresResolvedUriPathInCacheIfNotFoundInCache() $mockRoute2->expects(self::once())->method('resolves')->with($resolveContext)->willReturn(true); $mockRoute2->expects(self::atLeastOnce())->method('getResolvedUriConstraints')->willReturn($mockResolvedUriConstraints); - $router->_set('routes', [$mockRoute1, $mockRoute2]); + $mockRoutesProvider = $this->createMock(RoutesProviderInterface::class); + $mockRoutesProvider->expects($this->once())->method("getRoutes")->willReturn(Routes::create($mockRoute1, $mockRoute2)); + $this->inject($this->router, 'routesProvider', $mockRoutesProvider); + + $mockRouterCachingService = $this->createMock(RouterCachingService::class); + $mockRouterCachingService->expects(self::once())->method('getCachedResolvedUriConstraints')->with($resolveContext)->willReturn(false); + $mockRouterCachingService->expects(self::once())->method('storeResolvedUriConstraints')->with($resolveContext, $mockResolvedUriConstraints, null, null); + $this->inject($this->router, 'routerCachingService', $mockRouterCachingService); - $this->mockRouterCachingService->expects(self::once())->method('storeResolvedUriConstraints')->with($resolveContext, $mockResolvedUriConstraints, null, null); - self::assertSame('/resolved/path', (string)$router->resolve($resolveContext)); + self::assertSame('/resolved/path', (string)$this->router->resolve($resolveContext)); } /** @@ -326,11 +232,6 @@ public function resolveStoresResolvedUriPathInCacheIfNotFoundInCache() */ public function resolveStoresResolvedUriPathInCacheIfNotFoundInCachWithTagsAndCacheLifetime() { - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['createRoutesFromConfiguration']); - $this->inject($router, 'routerCachingService', $this->mockRouterCachingService); - $this->inject($router, 'logger', $this->mockSystemLogger); - $routeValues = ['some' => 'route values']; $mockResolvedUriConstraints = UriConstraints::create()->withPath('resolved/path'); @@ -345,10 +246,17 @@ public function resolveStoresResolvedUriPathInCacheIfNotFoundInCachWithTagsAndCa $mockRoute2->expects(self::atLeastOnce())->method('getResolvedUriConstraints')->willReturn($mockResolvedUriConstraints); $mockRoute2->expects(self::atLeastOnce())->method('getResolvedTags')->willReturn($routeTags); $mockRoute2->expects(self::atLeastOnce())->method('getResolvedLifetime')->willReturn($routeLifetime); - $router->_set('routes', [$mockRoute1, $mockRoute2]); - $this->mockRouterCachingService->expects(self::once())->method('storeResolvedUriConstraints')->with($resolveContext, $mockResolvedUriConstraints, $routeTags, $routeLifetime); - self::assertSame('/resolved/path', (string)$router->resolve($resolveContext)); + $mockRoutesProvider = $this->createMock(RoutesProviderInterface::class); + $mockRoutesProvider->expects($this->once())->method("getRoutes")->willReturn(Routes::create($mockRoute1, $mockRoute2)); + $this->inject($this->router, 'routesProvider', $mockRoutesProvider); + + $mockRouterCachingService = $this->createMock(RouterCachingService::class); + $mockRouterCachingService->expects($this->once())->method('getCachedResolvedUriConstraints')->with($resolveContext)->willReturn(false); + $mockRouterCachingService->expects($this->once())->method('storeResolvedUriConstraints')->with($resolveContext, $mockResolvedUriConstraints)->willReturn(false); + $this->inject($this->router, 'routerCachingService', $mockRouterCachingService); + + self::assertSame('/resolved/path', (string)$this->router->resolve($resolveContext)); } /** @@ -356,20 +264,14 @@ public function resolveStoresResolvedUriPathInCacheIfNotFoundInCachWithTagsAndCa */ public function routeReturnsCachedMatchResultsIfFoundInCache() { - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['createRoutesFromConfiguration']); - $this->inject($router, 'logger', $this->mockSystemLogger); - $routeContext = new RouteContext($this->mockHttpRequest, RouteParameters::createEmpty()); $cachedMatchResults = ['some' => 'cached results']; - $mockRouterCachingService = $this->getMockBuilder(RouterCachingService::class)->getMock(); - $mockRouterCachingService->expects(self::once())->method('getCachedMatchResults')->with($routeContext)->willReturn($cachedMatchResults); - $this->inject($router, 'routerCachingService', $mockRouterCachingService); - - $router->expects(self::never())->method('createRoutesFromConfiguration'); + $mockRouterCachingService = $this->createMock(RouterCachingService::class); + $mockRouterCachingService->method('getCachedMatchResults')->with($routeContext)->willReturn($cachedMatchResults); + $this->inject($this->router, 'routerCachingService', $mockRouterCachingService); - self::assertSame($cachedMatchResults, $router->route($routeContext)); + self::assertSame($cachedMatchResults, $this->router->route($routeContext)); } /** @@ -377,11 +279,6 @@ public function routeReturnsCachedMatchResultsIfFoundInCache() */ public function routeStoresMatchResultsInCacheIfNotFoundInCache() { - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['createRoutesFromConfiguration']); - $this->inject($router, 'routerCachingService', $this->mockRouterCachingService); - $this->inject($router, 'logger', $this->mockSystemLogger); - $matchResults = ['some' => 'match results']; $routeContext = new RouteContext($this->mockHttpRequest, RouteParameters::createEmpty()); @@ -391,11 +288,16 @@ public function routeStoresMatchResultsInCacheIfNotFoundInCache() $mockRoute2->expects(self::once())->method('matches')->with($routeContext)->willReturn(true); $mockRoute2->expects(self::once())->method('getMatchResults')->willReturn($matchResults); - $router->_set('routes', [$mockRoute1, $mockRoute2]); + $mockRoutesProvider = $this->createMock(RoutesProviderInterface::class); + $mockRoutesProvider->expects(self::once())->method("getRoutes")->willReturn(Routes::create($mockRoute1, $mockRoute2)); + $this->inject($this->router, 'routesProvider', $mockRoutesProvider); - $this->mockRouterCachingService->expects(self::once())->method('storeMatchResults')->with($routeContext, $matchResults, null, null); + $mockRouterCachingService = $this->createMock(RouterCachingService::class); + $mockRouterCachingService->expects(self::once())->method('getCachedMatchResults')->with($routeContext)->willReturn(false); + $mockRouterCachingService->expects(self::once())->method('storeMatchResults')->with($routeContext, $matchResults, null, null); + $this->inject($this->router, 'routerCachingService', $mockRouterCachingService); - self::assertSame($matchResults, $router->route($routeContext)); + self::assertSame($matchResults, $this->router->route($routeContext)); } /** @@ -403,11 +305,6 @@ public function routeStoresMatchResultsInCacheIfNotFoundInCache() */ public function routeStoresMatchResultsInCacheIfNotFoundInCacheWithTagsAndCacheLifetime() { - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['createRoutesFromConfiguration']); - $this->inject($router, 'routerCachingService', $this->mockRouterCachingService); - $this->inject($router, 'logger', $this->mockSystemLogger); - $matchResults = ['some' => 'match results']; $routeContext = new RouteContext($this->mockHttpRequest, RouteParameters::createEmpty()); $routeTags = RouteTags::createFromArray(['foo', 'bar']); @@ -421,11 +318,16 @@ public function routeStoresMatchResultsInCacheIfNotFoundInCacheWithTagsAndCacheL $mockRoute2->expects(self::once())->method('getMatchedTags')->willReturn($routeTags); $mockRoute2->expects(self::once())->method('getMatchedLifetime')->willReturn($routeLifetime); - $router->_set('routes', [$mockRoute1, $mockRoute2]); + $mockRoutesProvider = $this->createMock(RoutesProviderInterface::class); + $mockRoutesProvider->expects(self::once())->method("getRoutes")->willReturn(Routes::create($mockRoute1, $mockRoute2)); + $this->inject($this->router, 'routesProvider', $mockRoutesProvider); - $this->mockRouterCachingService->expects(self::once())->method('storeMatchResults')->with($routeContext, $matchResults, $routeTags, $routeLifetime); + $mockRouterCachingService = $this->createMock(RouterCachingService::class); + $mockRouterCachingService->expects(self::once())->method('getCachedMatchResults')->with($routeContext)->willReturn(false); + $mockRouterCachingService->expects(self::once())->method('storeMatchResults')->with($routeContext, $matchResults, $routeTags, $routeLifetime); + $this->inject($this->router, 'routerCachingService', $mockRouterCachingService); - self::assertSame($matchResults, $router->route($routeContext)); + self::assertSame($matchResults, $this->router->route($routeContext)); } /** @@ -441,11 +343,6 @@ public function getLastMatchedRouteReturnsNullByDefault() */ public function routeSetsLastMatchedRoute() { - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['createRoutesFromConfiguration']); - $this->inject($router, 'routerCachingService', $this->mockRouterCachingService); - $this->inject($router, 'logger', $this->mockSystemLogger); - $routeContext = new RouteContext($this->mockHttpRequest, RouteParameters::createEmpty()); $mockRoute1 = $this->getMockBuilder(Route::class)->getMock(); @@ -454,85 +351,12 @@ public function routeSetsLastMatchedRoute() $mockRoute2->expects(self::once())->method('matches')->with($routeContext)->willReturn(true); $mockRoute2->expects(self::once())->method('getMatchResults')->willReturn([]); - $router->_set('routes', [$mockRoute1, $mockRoute2]); + $mockRoutesProvider = $this->createMock(RoutesProviderInterface::class); + $mockRoutesProvider->expects($this->once())->method("getRoutes")->willReturn(Routes::create($mockRoute1, $mockRoute2)); + $this->inject($this->router, 'routesProvider', $mockRoutesProvider); - $router->route($routeContext); + $this->router->route($routeContext); - self::assertSame($mockRoute2, $router->getLastMatchedRoute()); - } - - /** - * @test - */ - public function routeLoadsRoutesConfigurationFromConfigurationManagerIfNotSetExplicitly() - { - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['dummy']); - $this->inject($router, 'routerCachingService', $this->mockRouterCachingService); - $this->inject($router, 'logger', $this->mockSystemLogger); - - $uri = new Uri('http://localhost/'); - $this->mockHttpRequest->expects(self::any())->method('getUri')->willReturn($uri); - - $routesConfiguration = [ - [ - 'uriPattern' => 'some/uri/pattern', - ], - [ - 'uriPattern' => 'some/other/uri/pattern', - ], - ]; - - /** @var ConfigurationManager|\PHPUnit\Framework\MockObject\MockObject $mockConfigurationManager */ - $mockConfigurationManager = $this->getMockBuilder(ConfigurationManager::class)->disableOriginalConstructor()->getMock(); - $mockConfigurationManager->expects(self::once())->method('getConfiguration')->with(ConfigurationManager::CONFIGURATION_TYPE_ROUTES)->willReturn($routesConfiguration); - $this->inject($router, 'configurationManager', $mockConfigurationManager); - - try { - $router->route(new RouteContext($this->mockHttpRequest, RouteParameters::createEmpty())); - } catch (NoMatchingRouteException $exception) { - } - - $routes = $router->getRoutes(); - $firstRoute = reset($routes); - self::assertSame('some/uri/pattern', $firstRoute->getUriPattern()); - } - - /** - * @test - */ - public function routeDoesNotLoadRoutesConfigurationFromConfigurationManagerIfItsSetExplicitly() - { - /** @var Router|\PHPUnit\Framework\MockObject\MockObject $router */ - $router = $this->getAccessibleMock(Router::class, ['dummy']); - $this->inject($router, 'routerCachingService', $this->mockRouterCachingService); - $this->inject($router, 'logger', $this->mockSystemLogger); - - $uri = new Uri('http://localhost/'); - $this->mockHttpRequest->expects(self::any())->method('getUri')->willReturn($uri); - - $routesConfiguration = [ - [ - 'uriPattern' => 'some/uri/pattern', - ], - [ - 'uriPattern' => 'some/other/uri/pattern', - ], - ]; - - /** @var ConfigurationManager|\PHPUnit\Framework\MockObject\MockObject $mockConfigurationManager */ - $mockConfigurationManager = $this->getMockBuilder(ConfigurationManager::class)->disableOriginalConstructor()->getMock(); - $mockConfigurationManager->expects(self::never())->method('getConfiguration'); - $this->inject($router, 'configurationManager', $mockConfigurationManager); - - $router->setRoutesConfiguration($routesConfiguration); - try { - $router->route(new RouteContext($this->mockHttpRequest, RouteParameters::createEmpty())); - } catch (NoMatchingRouteException $exception) { - } - - $routes = $router->getRoutes(); - $firstRoute = reset($routes); - self::assertSame('some/uri/pattern', $firstRoute->getUriPattern()); + self::assertSame($mockRoute2, $this->router->getLastMatchedRoute()); } } diff --git a/Neos.Flow/Tests/Unit/Mvc/Routing/RoutesTest.php b/Neos.Flow/Tests/Unit/Mvc/Routing/RoutesTest.php new file mode 100644 index 0000000000..86bc766926 --- /dev/null +++ b/Neos.Flow/Tests/Unit/Mvc/Routing/RoutesTest.php @@ -0,0 +1,166 @@ +assertSame([], iterator_to_array($routes)); + } + + /** + * @test + */ + public function fromConfigurationWorksAsExpected(): void + { + $route1 = new Route(); + $route1->setName('Route 1'); + $route1->setUriPattern('route1/{@package}/{@controller}/{@action}(.{@format})'); + $route1->setDefaults(['@format' => 'html']); + + $route2 = new Route(); + $route2->setName('Route 2'); + $route2->setDefaults(['@format' => 'html']); + $route2->setUriPattern('route2/{@package}/{@controller}/{@action}(.{@format})'); + $route2->setLowerCase(false); + $route2->setAppendExceedingArguments(true); + $route2->setRoutePartsConfiguration( + [ + '@controller' => [ + 'handler' => 'MyRoutePartHandler' + ] + ] + ); + $route2->setHttpMethods(['PUT']); + $route2->setCacheTags(Routing\Dto\RouteTags::createFromArray(['foo', 'bar'])); + $route2->setCacheLifetime(Routing\Dto\RouteLifetime::fromInt(10000)); + + $configuration = [ + [ + 'name' => 'Route 1', + 'uriPattern' => 'route1/{@package}/{@controller}/{@action}(.{@format})', + 'defaults' => ['@format' => 'html'] + ], + [ + 'name' => 'Route 2', + 'defaults' => ['@format' => 'html'], + 'uriPattern' => 'route2/{@package}/{@controller}/{@action}(.{@format})', + 'toLowerCase' => false, + 'appendExceedingArguments' => true, + 'routeParts' => [ + '@controller' => [ + 'handler' => 'MyRoutePartHandler' + ] + ], + 'httpMethods' => ['PUT'], + 'cache' => [ + 'lifetime' => 10000, + 'tags' => ['foo', 'bar'] + ], + ], + ]; + + $routes = Routes::fromConfiguration($configuration); + + $this->assertEquals( + [$route1, $route2], + iterator_to_array($routes) + ); + } + + /** + * @test + */ + public function mergeRoutes(): void + { + $route1 = new Route(); + $route1->setName("Route 1"); + + $route2 = new Route(); + $route2->setName("Route 2"); + + $routes = Routes::create($route1)->merge(Routes::create($route2)); + + $this->assertSame([$route1, $route2], iterator_to_array($routes)); + } + + /** + * @test + */ + public function createRoutesFromConfigurationThrowsExceptionIfOnlySomeRoutesWithTheSameUriPatternHaveHttpMethodConstraints() + { + // multiple routes with the uriPattern and "httpMethods" option + $this->expectException(InvalidRouteSetupException::class); + $routesConfiguration = [ + [ + 'uriPattern' => 'somePattern' + ], + [ + 'uriPattern' => 'somePattern', + 'httpMethods' => ['POST', 'PUT'] + ], + ]; + shuffle($routesConfiguration); + Routes::fromConfiguration($routesConfiguration); + } + + /** + * @test + */ + public function createRoutesFromConfigurationParsesTheGivenConfigurationAndBuildsRouteObjectsFromIt() + { + $routesConfiguration = []; + $routesConfiguration['route1']['uriPattern'] = 'number1'; + $routesConfiguration['route2']['uriPattern'] = 'number2'; + $routesConfiguration['route3'] = [ + 'name' => 'route3', + 'defaults' => ['foodefault'], + 'routeParts' => ['fooroutepart'], + 'uriPattern' => 'number3', + 'toLowerCase' => false, + 'appendExceedingArguments' => true, + 'httpMethods' => ['POST', 'PUT'] + ]; + + /** @var Route[] $createdRoutes */ + $createdRoutes = iterator_to_array(Routes::fromConfiguration($routesConfiguration)); + + self::assertEquals('number1', $createdRoutes[0]->getUriPattern()); + self::assertTrue($createdRoutes[0]->isLowerCase()); + self::assertFalse($createdRoutes[0]->getAppendExceedingArguments()); + self::assertEquals('number2', $createdRoutes[1]->getUriPattern()); + self::assertFalse($createdRoutes[1]->hasHttpMethodConstraints()); + self::assertEquals([], $createdRoutes[1]->getHttpMethods()); + self::assertEquals('route3', $createdRoutes[2]->getName()); + self::assertEquals(['foodefault'], $createdRoutes[2]->getDefaults()); + self::assertEquals(['fooroutepart'], $createdRoutes[2]->getRoutePartsConfiguration()); + self::assertEquals('number3', $createdRoutes[2]->getUriPattern()); + self::assertFalse($createdRoutes[2]->isLowerCase()); + self::assertTrue($createdRoutes[2]->getAppendExceedingArguments()); + self::assertTrue($createdRoutes[2]->hasHttpMethodConstraints()); + self::assertEquals(['POST', 'PUT'], $createdRoutes[2]->getHttpMethods()); + } +} diff --git a/Neos.Flow/Tests/Unit/Mvc/Routing/RoutingMiddlewareTest.php b/Neos.Flow/Tests/Unit/Mvc/Routing/RoutingMiddlewareTest.php index e7f8a58e3e..273cd38085 100644 --- a/Neos.Flow/Tests/Unit/Mvc/Routing/RoutingMiddlewareTest.php +++ b/Neos.Flow/Tests/Unit/Mvc/Routing/RoutingMiddlewareTest.php @@ -12,11 +12,12 @@ */ use GuzzleHttp\Psr7\Response; -use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Http\ServerRequestAttributes; use Neos\Flow\Mvc\Routing\Dto\RouteParameters; use Neos\Flow\Mvc\Routing\Dto\RouteContext; use Neos\Flow\Mvc\Routing\Router; +use Neos\Flow\Mvc\Routing\Routes; +use Neos\Flow\Mvc\Routing\RoutesProviderInterface; use Neos\Flow\Mvc\Routing\RoutingMiddleware; use Neos\Flow\Tests\UnitTestCase; use Psr\Http\Message\ServerRequestInterface; @@ -38,11 +39,6 @@ class RoutingMiddlewareTest extends UnitTestCase */ protected $mockRouter; - /** - * @var ConfigurationManager|\PHPUnit\Framework\MockObject\MockObject - */ - protected $mockConfigurationManager; - /** * @var RequestHandlerInterface|\PHPUnit\Framework\MockObject\MockObject */ @@ -66,9 +62,10 @@ protected function setUp(): void { $this->routingMiddleware = new RoutingMiddleware(); - $this->mockRouter = $this->getMockBuilder(Router::class)->getMock(); - $this->mockConfigurationManager = $this->getMockBuilder(ConfigurationManager::class)->disableOriginalConstructor()->getMock(); - $this->inject($this->mockRouter, 'configurationManager', $this->mockConfigurationManager); + $this->mockRouter = $this->createMock(Router::class); + $mockRoutesProvider = $this->createMock(RoutesProviderInterface::class); + $mockRoutesProvider->method('getRoutes')->willReturn(Routes::empty()); + $this->inject($this->mockRouter, 'routesProvider', $mockRoutesProvider); $this->inject($this->routingMiddleware, 'router', $this->mockRouter); diff --git a/Neos.Flow/Tests/Unit/Property/TypeConverter/ScalarTypeToObjectConverterTest.php b/Neos.Flow/Tests/Unit/Property/TypeConverter/ScalarTypeToObjectConverterTest.php index 71a44eb013..87005cf196 100644 --- a/Neos.Flow/Tests/Unit/Property/TypeConverter/ScalarTypeToObjectConverterTest.php +++ b/Neos.Flow/Tests/Unit/Property/TypeConverter/ScalarTypeToObjectConverterTest.php @@ -16,6 +16,7 @@ require_once(__DIR__ . '/../../Fixtures/ClassWithBoolConstructor.php'); use Neos\Flow\Fixtures\ClassWithBoolConstructor; +use Neos\Flow\Fixtures\ClassWithFloatConstructor; use Neos\Flow\Fixtures\ClassWithIntegerConstructor; use Neos\Flow\Fixtures\ClassWithStringConstructor; use Neos\Flow\Property\TypeConverter\ScalarTypeToObjectConverter; @@ -108,4 +109,21 @@ public function canConvertFromIntegerToValueObject() $canConvert = $converter->canConvertFrom(42, ClassWithIntegerConstructor::class); self::assertTrue($canConvert); } + + /** + * @test + */ + public function canConvertFromFloatToValueObject() + { + $converter = new ScalarTypeToObjectConverter(); + + $this->reflectionMock->expects(self::once()) + ->method('getMethodParameters') + ->willReturn([[ + 'type' => 'float' + ]]); + $this->inject($converter, 'reflectionService', $this->reflectionMock); + $canConvert = $converter->canConvertFrom(4.2, ClassWithFloatConstructor::class); + self::assertTrue($canConvert); + } } diff --git a/Neos.Flow/composer.json b/Neos.Flow/composer.json index 0683a98a6b..aea18f0e1e 100644 --- a/Neos.Flow/composer.json +++ b/Neos.Flow/composer.json @@ -56,8 +56,7 @@ "composer/composer": "^2.2.8", - "egulias/email-validator": "^2.1.17 || ^3.0", - "enshrined/svg-sanitize": "^0.16.0" + "egulias/email-validator": "^2.1.17 || ^3.0" }, "require-dev": { "mikey179/vfsstream": "^1.6.10", diff --git a/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetController.php b/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetController.php index ac4e300466..6698f07a7b 100644 --- a/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetController.php +++ b/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetController.php @@ -12,7 +12,6 @@ */ use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\ActionResponse; use Neos\Flow\Mvc\Controller\ActionController; use Neos\Flow\Mvc\Exception\ForwardException; use Neos\Flow\Mvc\Exception\InvalidActionVisibilityException; @@ -24,6 +23,7 @@ use Neos\Flow\Property\Exception; use Neos\Flow\Persistence\QueryResultInterface; use Neos\FluidAdaptor\Core\Widget\Exception\WidgetContextNotFoundException; +use Psr\Http\Message\ResponseInterface; /** * This is the base class for all widget controllers. @@ -46,7 +46,7 @@ abstract class AbstractWidgetController extends ActionController * Handles a request. The result output is returned by altering the given response. * * @param ActionRequest $request The request object - * @return ActionResponse $response The response, modified by this handler + * @return ResponseInterface The response, created by this handler * @throws WidgetContextNotFoundException * @throws InvalidActionVisibilityException * @throws InvalidArgumentTypeException @@ -59,7 +59,7 @@ abstract class AbstractWidgetController extends ActionController * @throws \Neos\Flow\Security\Exception * @api */ - public function processRequest(ActionRequest $request): ActionResponse + public function processRequest(ActionRequest $request): ResponseInterface { /** @var WidgetContext $widgetContext */ $widgetContext = $request->getInternalArgument('__widgetContext'); diff --git a/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetViewHelper.php b/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetViewHelper.php index 3479a57fbf..81937f5fb2 100644 --- a/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetViewHelper.php +++ b/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetViewHelper.php @@ -12,7 +12,6 @@ */ use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\ActionResponse; use Neos\Flow\Mvc\Exception\ForwardException; use Neos\Flow\Mvc\Exception\InfiniteLoopException; use Neos\Flow\Mvc\Exception\StopActionException; @@ -230,7 +229,6 @@ protected function initiateSubRequest() if ($dispatchLoopCount++ > 99) { throw new InfiniteLoopException('Could not ultimately dispatch the widget request after ' . $dispatchLoopCount . ' iterations.', 1380282310); } - $subResponse = new ActionResponse(); $widgetControllerObjectName = $this->widgetContext->getControllerObjectName(); if ($subRequest->getControllerObjectName() !== '' && $subRequest->getControllerObjectName() !== $widgetControllerObjectName) { @@ -239,19 +237,43 @@ protected function initiateSubRequest() $subRequest->setControllerObjectName($this->widgetContext->getControllerObjectName()); try { $subResponse = $this->controller->processRequest($subRequest); - - // We need to make sure to not merge content up into the parent ActionResponse because that _could_ break the parent response. - $content = $subResponse->getContent(); - $subResponse->setContent(''); } catch (StopActionException $exception) { $subResponse = $exception->response; - $subResponse->mergeIntoParentResponse($this->controllerContext->getResponse()); - throw $exception; + $parentResponse = $this->controllerContext->getResponse()->buildHttpResponse(); + + // legacy behaviour of "mergeIntoParentResponse": + // transfer possible headers that have been set dynamically + foreach ($subResponse->getHeaders() as $name => $values) { + $parentResponse = $parentResponse->withHeader($name, $values); + } + // if the status code is 200 we assume it's the default and will not overrule it + if ($subResponse->getStatusCode() !== 200) { + $parentResponse = $parentResponse->withStatus($subResponse->getStatusCode()); + } + // if the known body size is not empty replace the body + if ($subResponse->getBody()->getSize() !== 0) { + $parentResponse = $parentResponse->withBody($subResponse->getBody()); + } + + throw StopActionException::createForResponse($parentResponse, 'Intercepted from widget view helper.'); } catch (ForwardException $exception) { $subRequest = $exception->nextRequest; continue; } - $subResponse->mergeIntoParentResponse($this->controllerContext->getResponse()); + + // We need to make sure to not merge content up into the parent ActionResponse because that _could_ break the parent response. + $content = $subResponse->getBody()->getContents(); + + // hacky, but part of the deal. Legacy behaviour of "mergeIntoParentResponse": + // we have to manipulate the global response to redirect for example. + // transfer possible headers that have been set dynamically + foreach ($subResponse->getHeaders() as $name => $values) { + $this->controllerContext->getResponse()->setHttpHeader($name, $values); + } + // if the status code is 200 we assume it's the default and will not overrule it + if ($subResponse->getStatusCode() !== 200) { + $this->controllerContext->getResponse()->setStatusCode($subResponse->getStatusCode()); + } } return $content; diff --git a/Neos.FluidAdaptor/Classes/Core/Widget/AjaxWidgetMiddleware.php b/Neos.FluidAdaptor/Classes/Core/Widget/AjaxWidgetMiddleware.php index 6641c8fbed..f5b2f45e06 100644 --- a/Neos.FluidAdaptor/Classes/Core/Widget/AjaxWidgetMiddleware.php +++ b/Neos.FluidAdaptor/Classes/Core/Widget/AjaxWidgetMiddleware.php @@ -79,7 +79,7 @@ public function process(ServerRequestInterface $httpRequest, RequestHandlerInter $actionRequest = $this->actionRequestFactory->createActionRequest($httpRequest, ['__widgetContext' => $widgetContext]); $actionRequest->setControllerObjectName($widgetContext->getControllerObjectName()); $this->securityContext->setRequest($actionRequest); - return $this->dispatcher->dispatch($actionRequest)->buildHttpResponse(); + return $this->dispatcher->dispatch($actionRequest); } /** diff --git a/Neos.FluidAdaptor/Tests/Functional/Core/WidgetTest.php b/Neos.FluidAdaptor/Tests/Functional/Core/WidgetTest.php index 93dcbc4813..e6367dd320 100644 --- a/Neos.FluidAdaptor/Tests/Functional/Core/WidgetTest.php +++ b/Neos.FluidAdaptor/Tests/Functional/Core/WidgetTest.php @@ -11,7 +11,6 @@ * source code. */ -use Neos\Flow\Mvc\Routing\Route; use Neos\Flow\Tests\FunctionalTestCase; /** @@ -26,17 +25,17 @@ protected function setUp(): void { parent::setUp(); - $route = new Route(); - $route->setName('WidgetTest'); - $route->setUriPattern('test/widget/{@controller}(/{@action})'); - $route->setDefaults([ - '@package' => 'Neos.FluidAdaptor', - '@subpackage' => 'Tests\Functional\Core\Fixtures', - '@action' => 'index', - '@format' => 'html' - ]); - $route->setAppendExceedingArguments(true); - $this->router->addRoute($route); + $this->registerRoute( + 'WidgetTest', + 'test/widget/{@controller}(/{@action})', + [ + '@package' => 'Neos.FluidAdaptor', + '@subpackage' => 'Tests\Functional\Core\Fixtures', + '@action' => 'index', + '@format' => 'html' + ], + true + ); } /** diff --git a/Neos.FluidAdaptor/Tests/Functional/Form/FormObjectsTest.php b/Neos.FluidAdaptor/Tests/Functional/Form/FormObjectsTest.php index 518f646d26..48eb7ed61f 100644 --- a/Neos.FluidAdaptor/Tests/Functional/Form/FormObjectsTest.php +++ b/Neos.FluidAdaptor/Tests/Functional/Form/FormObjectsTest.php @@ -11,7 +11,6 @@ * source code. */ -use Neos\Flow\Mvc\Routing\Route; /** * Testcase for Standalone View @@ -37,17 +36,18 @@ protected function setUp(): void { parent::setUp(); - $route = new Route(); - $route->setUriPattern('test/fluid/formobjects(/{@action})'); - $route->setDefaults([ - '@package' => 'Neos.FluidAdaptor', - '@subpackage' => 'Tests\Functional\Form\Fixtures', - '@controller' => 'Form', - '@action' => 'index', - '@format' => 'html' - ]); - $route->setAppendExceedingArguments(true); - $this->router->addRoute($route); + $this->registerRoute( + 'Form Test Route', + 'test/fluid/formobjects(/{@action})', + [ + '@package' => 'Neos.FluidAdaptor', + '@subpackage' => 'Tests\Functional\Form\Fixtures', + '@controller' => 'Form', + '@action' => 'index', + '@format' => 'html' + ], + true + ); } /** diff --git a/Neos.FluidAdaptor/Tests/Unit/Core/Widget/AbstractWidgetControllerTest.php b/Neos.FluidAdaptor/Tests/Unit/Core/Widget/AbstractWidgetControllerTest.php index a18502df05..2557fbb45f 100644 --- a/Neos.FluidAdaptor/Tests/Unit/Core/Widget/AbstractWidgetControllerTest.php +++ b/Neos.FluidAdaptor/Tests/Unit/Core/Widget/AbstractWidgetControllerTest.php @@ -11,6 +11,7 @@ * source code. */ +use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\ServerRequest; use GuzzleHttp\Psr7\Uri; use Neos\Flow\Mvc\ActionResponse; @@ -48,7 +49,6 @@ public function processRequestShouldSetWidgetConfiguration() { /** @var \Neos\Flow\Mvc\ActionRequest $mockActionRequest */ $mockActionRequest = $this->createMock(\Neos\Flow\Mvc\ActionRequest::class); - $mockResponse = new ActionResponse(); $httpRequest = new ServerRequest('GET', new Uri('http://localhost')); $mockActionRequest->expects(self::any())->method('getHttpRequest')->will(self::returnValue($httpRequest)); @@ -64,7 +64,7 @@ public function processRequestShouldSetWidgetConfiguration() $abstractWidgetController = $this->getAccessibleMock(\Neos\FluidAdaptor\Core\Widget\AbstractWidgetController::class, ['resolveActionMethodName', 'initializeActionMethodArguments', 'initializeActionMethodValidators', 'mapRequestArgumentsToControllerArguments', 'detectFormat', 'resolveView', 'callActionMethod']); $abstractWidgetController->method('resolveActionMethodName')->willReturn('indexAction'); $abstractWidgetController->_set('mvcPropertyMappingConfigurationService', $this->createMock(\Neos\Flow\Mvc\Controller\MvcPropertyMappingConfigurationService::class)); - $abstractWidgetController->expects(self::once())->method('callActionMethod')->willReturn($mockResponse); + $abstractWidgetController->expects(self::once())->method('callActionMethod')->willReturn(new Response()); $abstractWidgetController->processRequest($mockActionRequest); $actualWidgetConfiguration = $abstractWidgetController->_get('widgetConfiguration'); diff --git a/Neos.FluidAdaptor/Tests/Unit/Core/Widget/AjaxWidgetMiddlewareTest.php b/Neos.FluidAdaptor/Tests/Unit/Core/Widget/AjaxWidgetMiddlewareTest.php index 4dacef28f3..b0b3d55f58 100644 --- a/Neos.FluidAdaptor/Tests/Unit/Core/Widget/AjaxWidgetMiddlewareTest.php +++ b/Neos.FluidAdaptor/Tests/Unit/Core/Widget/AjaxWidgetMiddlewareTest.php @@ -14,7 +14,6 @@ use GuzzleHttp\Psr7\Response; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\ActionRequestFactory; -use Neos\Flow\Mvc\ActionResponse; use Neos\Flow\Mvc\Dispatcher; use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Flow\Security\Context; @@ -120,8 +119,7 @@ protected function setUp(): void $this->inject($this->ajaxWidgetMiddleware, 'hashService', $this->mockHashService); $this->mockDispatcher = $this->getMockBuilder(Dispatcher::class)->getMock(); - $actionResponse = new ActionResponse(); - $this->mockDispatcher->expects(self::any())->method('dispatch')->willReturn($actionResponse); + $this->mockDispatcher->expects(self::any())->method('dispatch')->willReturn(new Response()); $this->inject($this->ajaxWidgetMiddleware, 'dispatcher', $this->mockDispatcher); $this->mockSecurityContext = $this->getMockBuilder(Context::class)->getMock(); @@ -180,8 +178,7 @@ public function handleDispatchesActionRequestIfWidgetContextIsPresent() $mockActionRequest = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock(); $this->mockActionRequestFactory->method('prepareActionRequest')->willReturn($mockActionRequest); - $actionResponse = new ActionResponse(); - $this->mockDispatcher->expects(self::once())->method('dispatch')->willReturn($actionResponse); + $this->mockDispatcher->expects(self::once())->method('dispatch')->willReturn(new Response()); $this->ajaxWidgetMiddleware->process($this->mockHttpRequest, $this->mockRequestHandler); } diff --git a/Neos.Utility.Arrays/Classes/Arrays.php b/Neos.Utility.Arrays/Classes/Arrays.php index 015de973a6..e61ebf925d 100644 --- a/Neos.Utility.Arrays/Classes/Arrays.php +++ b/Neos.Utility.Arrays/Classes/Arrays.php @@ -217,7 +217,9 @@ public static function getValueByPath(array $array, array|string $path): mixed } /** - * Returns a type safe accessor for a value in a nested array by following the specifed path. + * Returns a type safe accessor for a value in a nested array by following the specified path. + * + * See {@see ValueAccessor} * * @param array $array The array to traverse * @param array|string $path The path to follow. Either a simple array of keys or a string in the format 'foo.bar.baz' @@ -225,9 +227,8 @@ public static function getValueByPath(array $array, array|string $path): mixed */ public static function getAccessorByPath(array $array, array|string $path): ValueAccessor { - $pathinfo = is_array($path) ? implode('.', $path) : $path; $value = self::getValueByPath($array, $path); - return new ValueAccessor($value, $pathinfo); + return ValueAccessor::forValueInPath($value, $path); } /** diff --git a/Neos.Utility.Arrays/Classes/ValueAccessor.php b/Neos.Utility.Arrays/Classes/ValueAccessor.php index e078fbd311..ba73867d99 100644 --- a/Neos.Utility.Arrays/Classes/ValueAccessor.php +++ b/Neos.Utility.Arrays/Classes/ValueAccessor.php @@ -1,5 +1,7 @@ int(); + * ``` + * + * Or in combination with {@see Arrays::getAccessorByPath()} to access values inside an array + * + * ```php + * $intValue = Arrays::getAccessorByPath($mixedArray, 'foo.myIntOption')->int(); + * ``` + * + * @api */ -class ValueAccessor +final readonly class ValueAccessor { - public function __construct( - public readonly mixed $value, - public readonly ?string $pathinfo = null + private function __construct( + public mixed $value, + private ?string $additionalErrorMessage ) { } - private function createTypeError($message): \UnexpectedValueException + public static function forValue(mixed $value): self { - return new \UnexpectedValueException(get_debug_type($this->value) . ' ' . $message . ($this->pathinfo ? ' in path ' . $this->pathinfo : '')); + return new self($value, null); + } + + /** + * @internal You should use {@see ValueAccessor::forValue} instead + */ + public static function forValueInPath(mixed $value, array|string $path): self + { + $pathinfo = is_array($path) ? implode('.', $path) : $path; + return new self($value, 'in path ' . $pathinfo); } public function int(): int @@ -155,4 +178,9 @@ public function instanceOfOrNull(string $className): ?object } throw $this->createTypeError(sprintf('is not an instance of %s or null', $className)); } + + private function createTypeError($message): \UnexpectedValueException + { + return new \UnexpectedValueException(get_debug_type($this->value) . ' ' . $message . ($this->additionalErrorMessage ? ' ' . $this->additionalErrorMessage : '')); + } } diff --git a/Neos.Utility.Arrays/Tests/Unit/ValueAccessorTest.php b/Neos.Utility.Arrays/Tests/Unit/ValueAccessorTest.php index f5cafc4132..36e728f245 100644 --- a/Neos.Utility.Arrays/Tests/Unit/ValueAccessorTest.php +++ b/Neos.Utility.Arrays/Tests/Unit/ValueAccessorTest.php @@ -12,7 +12,6 @@ * source code. */ -use Neos\Utility\Arrays; use Neos\Utility\ValueAccessor; /** @@ -111,19 +110,19 @@ public function instanceOfAccessorWorks() } protected function testAccessor( - array $acceptibleValues, - array $inacceptibleValues, + array $acceptibleValues, + array $inacceptibleValues, string $methodName, - array $methodArguments = [], + array $methodArguments = [], ): void { foreach ($acceptibleValues as $value) { - $accessor = new ValueAccessor($value, (is_scalar($value) || $value instanceof \Stringable) ? (string)$value : get_debug_type($value) . 'was given'); + $accessor = ValueAccessor::forValue($value); $result = $accessor->$methodName(...$methodArguments); $this->assertEquals($value, $result); } foreach ($inacceptibleValues as $value) { $this->expectException(\UnexpectedValueException::class); - $accessor = new ValueAccessor($value, (is_scalar($value) || $value instanceof \Stringable) ? (string)$value : get_debug_type($value) . 'was given'); + $accessor = ValueAccessor::forValue($value); $accessor->$methodName(...$methodArguments); } } diff --git a/Readme.rst b/Readme.rst index b990c058c7..82bf0a2e83 100644 --- a/Readme.rst +++ b/Readme.rst @@ -54,6 +54,7 @@ Contributing If you want to contribute to Flow Framework and want to set up a development environment, then follow these steps: +Clone and install the flow dev distribution https://github.com/neos/flow-development-distribution via git and composer or use this shorthand: ``composer create-project neos/flow-development-distribution flow-development @dev --keep-vcs`` Note the **-distribution** package you create a project from, instead of just checking out this repository. @@ -65,8 +66,8 @@ Here you can do all Git-related work (``git add .``, ``git commit``, etc). Unit tests can be run here via ``../../bin/phpunit -c ../../Build/BuildEssentials/PhpUnit/UnitTests.xml``, functional tests via ``../../bin/phpunit -c ../../Build/BuildEssentials/PhpUnit/FunctionalTests.xml`` and static analysis via ``composer lint``. -To switch the branch you intend to work on: -``git checkout 6.3 && composer update`` +To switch the branch you intend to work on run this command in the root of the dev distribution: +``git checkout 8.3 && composer update`` .. note:: We use an upmerging strategy, so create all bugfixes to lowest maintained branch that contains the issue (typically the second last LTS release, check the diagram on diff --git a/composer.json b/composer.json index 839277a5c4..e818c8b427 100644 --- a/composer.json +++ b/composer.json @@ -10,8 +10,9 @@ "../../flow doctrine:migrate --quiet", "../../bin/behat -f progress -c Neos.Flow/Tests/Behavior/behat.yml" ], + "lint:phpstan": "../../bin/phpstan analyse", "lint": [ - "../../bin/phpstan analyse" + "@lint:phpstan" ] }, "require": { @@ -26,6 +27,7 @@ "ext-reflection": "*", "ext-xml": "*", "ext-xmlreader": "*", + "neos/composer-plugin": "^2.0", "psr/http-message": "^1.0", "psr/http-factory": "^1.0", "psr/container": "^1.0", @@ -42,7 +44,6 @@ "symfony/yaml": "^5.1||^6.0", "symfony/dom-crawler": "^5.1||^6.0", "symfony/console": "^5.1||^6.0", - "neos/composer-plugin": "^2.0", "composer/composer": "^2.2.8", "egulias/email-validator": "^2.1.17 || ^3.0", "typo3fluid/fluid": "^2.8.0", @@ -138,6 +139,11 @@ "mikey179/vfsstream": "^1.6.10", "phpunit/phpunit": "~9.1" }, + "provide": { + "psr/cache-implementation": "2.0.0 || 3.0.0", + "psr/simple-cache-implementation": "2.0.0 || 3.0.0", + "psr/log-implementation": "2.0.0 || 3.0.0" + }, "autoload-dev": { "psr-4": { "Neos\\Cache\\Tests\\": [ diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2d8a0153ee..00e2d85269 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,9 +1,5 @@ parameters: level: 2 - ignoreErrors: - # https://github.com/phpstan/phpstan/issues/9467 will be fixed with flow 9 - - '#^Method Neos\\Flow\\Persistence\\QueryInterface\:\:logicalOr\(\) invoked with \d parameters, 1 required\.$#' - - '#^Method Neos\\Flow\\Persistence\\QueryInterface\:\:logicalAnd\(\) invoked with \d parameters, 1 required\.$#' paths: - Neos.Cache/Classes - Neos.Eel/Classes