diff --git a/Neos.Cache/Classes/Backend/AbstractBackend.php b/Neos.Cache/Classes/Backend/AbstractBackend.php index 05dab6c87b..51f1582c88 100644 --- a/Neos.Cache/Classes/Backend/AbstractBackend.php +++ b/Neos.Cache/Classes/Backend/AbstractBackend.php @@ -28,18 +28,18 @@ abstract class AbstractBackend implements BackendInterface /** * Reference to the cache frontend which uses this backend - * @var FrontendInterface + * @var ?FrontendInterface */ protected $cache; /** - * @var string + * @var ?string */ protected $cacheIdentifier; /** - * A prefix to seperate stored by appliaction context and cache - * @var string + * A prefix to separate stored by application context and cache + * @var ?string */ protected $identifierPrefix; @@ -50,28 +50,25 @@ abstract class AbstractBackend implements BackendInterface protected $defaultLifetime = 3600; /** - * @var EnvironmentConfiguration + * @var ?EnvironmentConfiguration */ protected $environmentConfiguration; /** * Constructs this backend * - * @param EnvironmentConfiguration $environmentConfiguration - * @param array $options Configuration options - depends on the actual backend + * @param ?EnvironmentConfiguration $environmentConfiguration @todo is this ever null and does that even make sense? + * @param array $options Configuration options - depends on the actual backend * @api */ public function __construct(?EnvironmentConfiguration $environmentConfiguration = null, array $options = []) { $this->environmentConfiguration = $environmentConfiguration; - - if (is_array($options) || $options instanceof \Iterator) { - $this->setProperties($options); - } + $this->setProperties($options); } /** - * @param array $properties + * @param array $properties * @param boolean $throwExceptionIfPropertyNotSettable * @return void * @throws \InvalidArgumentException diff --git a/Neos.Cache/Classes/Backend/ApcuBackend.php b/Neos.Cache/Classes/Backend/ApcuBackend.php index 6d48971bef..718ff6b500 100644 --- a/Neos.Cache/Classes/Backend/ApcuBackend.php +++ b/Neos.Cache/Classes/Backend/ApcuBackend.php @@ -83,6 +83,10 @@ public function setCache(FrontendInterface $cache): void { parent::setCache($cache); + if ($this->environmentConfiguration === null) { + throw new \RuntimeException('The environment configuration is not set.', 1744537489); + } + $pathHash = substr(md5($this->environmentConfiguration->getApplicationIdentifier() . $cache->getIdentifier()), 0, 12); $this->identifierPrefix = 'Flow_' . $pathHash . '_'; } @@ -110,7 +114,7 @@ public function getPrefixedIdentifier(string $entryIdentifier): string * * @param string $entryIdentifier An identifier for this specific cache entry * @param string $data The data to be stored - * @param array $tags Tags to associate with this cache entry + * @param array $tags Tags to associate with this cache entry * @param int|null $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. * @return void * @throws Exception if no cache frontend has been set. @@ -263,7 +267,7 @@ public function flushByTags(array $tags): int * Associates the identifier with the given tags * * @param string $entryIdentifier - * @param array $tags + * @param array $tags * @return void */ protected function addIdentifierToTags(string $entryIdentifier, array $tags) @@ -391,6 +395,7 @@ public function valid(): bool * * @return void * @api + * @phpstan-assert \APCUIterator $this->cacheEntriesIterator */ #[\ReturnTypeWillChange] public function rewind() diff --git a/Neos.Cache/Classes/Backend/BackendInterface.php b/Neos.Cache/Classes/Backend/BackendInterface.php index fbb29e4228..734a0ccace 100644 --- a/Neos.Cache/Classes/Backend/BackendInterface.php +++ b/Neos.Cache/Classes/Backend/BackendInterface.php @@ -50,7 +50,7 @@ public function getPrefixedIdentifier(string $entryIdentifier): string; * * @param string $entryIdentifier An identifier for this specific cache entry * @param string $data The data to be stored - * @param array $tags Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored. + * @param array $tags Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored. * @param integer|null $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. * @return void * @throws \Neos\Cache\Exception if no cache frontend has been set. diff --git a/Neos.Cache/Classes/Backend/FileBackend.php b/Neos.Cache/Classes/Backend/FileBackend.php index 18a99b72aa..bbf9194408 100644 --- a/Neos.Cache/Classes/Backend/FileBackend.php +++ b/Neos.Cache/Classes/Backend/FileBackend.php @@ -48,7 +48,7 @@ class FileBackend extends SimpleFileBackend implements PhpCapableBackendInterfac protected $cacheEntryFileExtension = ''; /** - * @var array + * @var array @phpstan-ignore property.phpDocType (this backend seems to work differently) */ protected $cacheEntryIdentifiers = []; @@ -90,6 +90,9 @@ public function freeze(): void $cachePathAndFileName = $this->cacheDirectory . 'FrozenCache.data'; if ($this->useIgBinary === true) { $data = igbinary_serialize($this->cacheEntryIdentifiers); + if ($data === null) { + throw new \RuntimeException('Failed to serialize cache entry identifiers', 1744536663); + } } else { $data = serialize($this->cacheEntryIdentifiers); } @@ -140,8 +143,8 @@ public function setCache(FrontendInterface $cache): void * * @param string $entryIdentifier An identifier for this specific cache entry * @param string $data The data to be stored - * @param array $tags Tags to associate with this cache entry - * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. + * @param array $tags Tags to associate with this cache entry + * @param ?integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. * @return void * @throws \RuntimeException * @throws Exception if the directory does not exist or is not writable or exceeds the maximum allowed path length, or if no cache frontend has been set. diff --git a/Neos.Cache/Classes/Backend/IterableBackendInterface.php b/Neos.Cache/Classes/Backend/IterableBackendInterface.php index 7815216fba..238efc397d 100644 --- a/Neos.Cache/Classes/Backend/IterableBackendInterface.php +++ b/Neos.Cache/Classes/Backend/IterableBackendInterface.php @@ -14,6 +14,7 @@ /** * A contract for a cache backend which provides Iterator functionality. * + * @extends \Iterator * @api */ interface IterableBackendInterface extends BackendInterface, \Iterator diff --git a/Neos.Cache/Classes/Backend/IterableMultiBackend.php b/Neos.Cache/Classes/Backend/IterableMultiBackend.php index 40ab0e408e..2e13e2356c 100644 --- a/Neos.Cache/Classes/Backend/IterableMultiBackend.php +++ b/Neos.Cache/Classes/Backend/IterableMultiBackend.php @@ -45,6 +45,9 @@ public function current(): mixed $this->prepareBackends(); foreach ($this->backends as $backend) { try { + if (!method_exists($backend, 'current')) { + throw new \RuntimeException(get_class($backend) . ' does not implement rewind'); + } return $backend->current(); } catch (Throwable $throwable) { $this->logger?->error('Failed retrieving current cache entry using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); @@ -63,6 +66,9 @@ public function next(): void $this->prepareBackends(); foreach ($this->backends as $backend) { try { + if (!method_exists($backend, 'next')) { + throw new \RuntimeException(get_class($backend) . ' does not implement rewind'); + } $backend->next(); } catch (Throwable $throwable) { $this->logger?->error('Failed retrieving next cache entry using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); @@ -80,6 +86,9 @@ public function key(): string|int|bool|null|float $this->prepareBackends(); foreach ($this->backends as $backend) { try { + if (!method_exists($backend, 'key')) { + throw new \RuntimeException(get_class($backend) . ' does not implement rewind'); + } return $backend->key(); } catch (Throwable $throwable) { $this->logger?->error('Failed retrieving cache entry key using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); @@ -98,6 +107,9 @@ public function valid(): bool $this->prepareBackends(); foreach ($this->backends as $backend) { try { + if (!method_exists($backend, 'valid')) { + throw new \RuntimeException(get_class($backend) . ' does not implement rewind'); + } return $backend->valid(); } catch (Throwable $throwable) { $this->logger?->error('Failed checking if current cache entry is valid using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); @@ -116,6 +128,9 @@ public function rewind(): void $this->prepareBackends(); foreach ($this->backends as $backend) { try { + if (!method_exists($backend, 'rewind')) { + throw new \RuntimeException(get_class($backend) . ' does not implement rewind'); + } $backend->rewind(); } catch (Throwable $throwable) { $this->logger?->error('Failed rewinding cache entries using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); diff --git a/Neos.Cache/Classes/Backend/MemcachedBackend.php b/Neos.Cache/Classes/Backend/MemcachedBackend.php index af6adecaa9..08ab205a19 100644 --- a/Neos.Cache/Classes/Backend/MemcachedBackend.php +++ b/Neos.Cache/Classes/Backend/MemcachedBackend.php @@ -65,7 +65,7 @@ class MemcachedBackend extends IndependentAbstractBackend implements TaggableBac /** * Array of Memcache server configurations * - * @var array + * @var array */ protected $servers = []; @@ -102,7 +102,7 @@ public function __construct(EnvironmentConfiguration $environmentConfiguration, * Setter for servers to be used. Expects an array, the values are expected * to be formatted like "[:]" or "unix://" * - * @param array $servers An array of servers to add. + * @param array $servers An array of servers to add. * @return void * @throws Exception * @api @@ -121,12 +121,12 @@ protected function setServers(array $servers) $host = $server; $port = 0; - if (strpos($server, 'tcp://') === 0) { + if (str_starts_with($server, 'tcp://')) { $port = $defaultPort; $server = substr($server, 6); } - if (strpos($server, ':') !== false) { + if (str_contains($server, ':')) { [$host, $portValue] = explode(':', $server, 2); $port = (int)$portValue; } @@ -165,6 +165,10 @@ public function setCache(FrontendInterface $cache): void { parent::setCache($cache); + if ($this->environmentConfiguration === null) { + throw new \RuntimeException('No environment configuration set', 1744535714); + } + $pathHash = substr(md5($this->environmentConfiguration->getApplicationIdentifier() . $cache->getIdentifier()), 0, 12); $this->identifierPrefix = 'Flow_' . $pathHash . '_'; } @@ -192,8 +196,8 @@ public function getPrefixedIdentifier(string $entryIdentifier): string * * @param string $entryIdentifier An identifier for this specific cache entry * @param string $data The data to be stored - * @param array $tags Tags to associate with this cache entry - * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. + * @param array $tags Tags to associate with this cache entry + * @param ?integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. * @return void * @throws Exception if no cache frontend has been set. * @throws \InvalidArgumentException if the identifier is not valid or the final memcached key is longer than 250 characters @@ -325,7 +329,7 @@ public function findIdentifiersByTag(string $tag): array * index to search for tags. * * @param string $identifier Identifier to find tags by - * @return array Array with tags + * @return array Array with tags */ protected function findTagsByIdentifier(string $identifier): array { @@ -383,7 +387,7 @@ public function flushByTags(array $tags): int * Associates the identifier with the given tags * * @param string $entryIdentifier - * @param array $tags + * @param array $tags * @return void */ protected function addIdentifierToTags(string $entryIdentifier, array $tags) diff --git a/Neos.Cache/Classes/Backend/MultiBackend.php b/Neos.Cache/Classes/Backend/MultiBackend.php index 4fc3715079..1d5293dd88 100644 --- a/Neos.Cache/Classes/Backend/MultiBackend.php +++ b/Neos.Cache/Classes/Backend/MultiBackend.php @@ -30,7 +30,13 @@ class MultiBackend extends AbstractBackend { use BackendInstantiationTrait; + /** + * @var array + */ protected array $backendConfigurations = []; + /** + * @var array + */ protected array $backends = []; protected bool $setInAllBackends = true; protected bool $debug = false; @@ -48,10 +54,8 @@ public function __construct(?EnvironmentConfiguration $environmentConfiguration if ($this->logErrors && class_exists(Bootstrap::class) && Bootstrap::$staticObjectManager instanceof ObjectManagerInterface) { try { $logger = Bootstrap::$staticObjectManager->get(LoggerInterface::class); - assert($logger instanceof LoggerInterface); $this->logger = $logger; $throwableStorage = Bootstrap::$staticObjectManager->get(ThrowableStorageInterface::class); - assert($throwableStorage instanceof ThrowableStorageInterface); $this->throwableStorage = $throwableStorage; } catch (UnknownObjectException) { // Logging might not be available during compile time @@ -78,11 +82,18 @@ protected function prepareBackends(): void } /** + * @param array $backendOptions * @throws Throwable */ protected function buildSubBackend(string $backendClassName, array $backendOptions): ?BackendInterface { try { + if ($this->cache === null) { + throw new \RuntimeException('Cache frontend is not yet initialized', 1744535490); + } + if ($this->environmentConfiguration === null) { + throw new \RuntimeException('Environment configuration is missing', 1744535532); + } $backend = $this->instantiateBackend($backendClassName, $backendOptions, $this->environmentConfiguration); $backend->setCache($this->cache); } catch (Throwable $throwable) { @@ -95,6 +106,7 @@ protected function buildSubBackend(string $backendClassName, array $backendOptio /** * @throws Throwable + * @param array $tags */ public function set(string $entryIdentifier, string $data, array $tags = [], ?int $lifetime = null): void { @@ -204,6 +216,7 @@ public function collectGarbage(): void /** * This setter is used by AbstractBackend::setProperties() + * @param array $backendConfigurations */ protected function setBackendConfigurations(array $backendConfigurations): void { diff --git a/Neos.Cache/Classes/Backend/NullBackend.php b/Neos.Cache/Classes/Backend/NullBackend.php index e01c90292e..2148fad548 100644 --- a/Neos.Cache/Classes/Backend/NullBackend.php +++ b/Neos.Cache/Classes/Backend/NullBackend.php @@ -41,8 +41,8 @@ protected function setProperty(string $propertyName, $propertyValue) : bool * * @param string $entryIdentifier ignored * @param string $data ignored - * @param array $tags ignored - * @param integer $lifetime ignored + * @param array $tags ignored + * @param ?integer $lifetime ignored * @return void * @api */ diff --git a/Neos.Cache/Classes/Backend/PdoBackend.php b/Neos.Cache/Classes/Backend/PdoBackend.php index 5c772a6dc5..12cda08799 100644 --- a/Neos.Cache/Classes/Backend/PdoBackend.php +++ b/Neos.Cache/Classes/Backend/PdoBackend.php @@ -48,7 +48,7 @@ class PdoBackend extends IndependentAbstractBackend implements TaggableBackendIn protected $password; /** - * @var array + * @var array */ protected $driverOptions = []; @@ -83,7 +83,7 @@ class PdoBackend extends IndependentAbstractBackend implements TaggableBackendIn protected $batchSize = 999; /** - * @var \ArrayIterator|null + * @var \ArrayIterator|null */ protected $cacheEntriesIterator; @@ -126,7 +126,7 @@ protected function setPassword(string $password): void /** * Sets the driverOptions to use * - * @param array $driverOptions The options to use for connecting to the DB + * @param array $driverOptions The options to use for connecting to the DB * @return void * @api */ @@ -174,7 +174,7 @@ protected function setBatchSize(int $batchSize): void * * @param string $entryIdentifier An identifier for this specific cache entry * @param string $data The data to be stored - * @param array $tags Tags to associate with this cache entry + * @param array $tags Tags to associate with this cache entry * @param int|null $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. * @return void * @throws Exception if no cache frontend has been set. @@ -613,6 +613,7 @@ public function valid(): bool * * @return void * @api + * @phpstan-assert \ArrayIterator $this->cacheEntriesIterator */ public function rewind(): void { @@ -647,6 +648,9 @@ public function rewind(): void protected function context(): string { if ($this->context === null) { + if ($this->environmentConfiguration === null) { + throw new \RuntimeException('Environment configuration not set', 1744534618); + } $this->context = md5($this->environmentConfiguration->getApplicationIdentifier()); } return $this->context; diff --git a/Neos.Cache/Classes/Backend/RedisBackend.php b/Neos.Cache/Classes/Backend/RedisBackend.php index 329f96de5e..3aca115e6a 100644 --- a/Neos.Cache/Classes/Backend/RedisBackend.php +++ b/Neos.Cache/Classes/Backend/RedisBackend.php @@ -79,7 +79,7 @@ class RedisBackend extends IndependentAbstractBackend implements TaggableBackend protected int $batchSize = 100000; /** - * @var \ArrayIterator|null + * @var \ArrayIterator|null */ private $entryIterator; @@ -87,15 +87,14 @@ class RedisBackend extends IndependentAbstractBackend implements TaggableBackend * Constructs this backend * * @param EnvironmentConfiguration $environmentConfiguration - * @param array $options Configuration options - depends on the actual backend + * @param array $options Configuration options - depends on the actual backend * @throws CacheException + * @phpstan-assert EnvironmentConfiguration $this->environmentConfiguration */ public function __construct(EnvironmentConfiguration $environmentConfiguration, array $options) { parent::__construct($environmentConfiguration, $options); - if (!$this->redis instanceof \Redis) { - $this->redis = $this->getRedisClient(); - } + $this->redis = $this->getRedisClient(); } /** @@ -103,7 +102,7 @@ public function __construct(EnvironmentConfiguration $environmentConfiguration, * * @param string $entryIdentifier An identifier for this specific cache entry * @param string $data The data to be stored - * @param array $tags Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored. + * @param array $tags Tags to associate with this cache entry. If the backend does not support tags, this option can be ignored. * @param integer|null $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. * @throws \RuntimeException * @throws CacheException @@ -425,7 +424,6 @@ public function freeze(): void foreach ($iterator as $entryIdentifier) { $this->redis->persist($this->getPrefixedIdentifier('entry:' . $entryIdentifier)); } - /** @var array|bool $result */ $result = $this->redis->exec(); $this->redis->set($this->getPrefixedIdentifier('frozen'), 1); } while ($result === false); @@ -510,7 +508,13 @@ private function uncompress(bool|string $value): bool|string private function compress(string $value): string { - return $this->useCompression() ? gzencode($value, $this->compressionLevel) : $value; + if ($this->useCompression()) { + $value = gzencode($value, $this->compressionLevel); + if ($value === false) { + throw new \RuntimeException('Failed to compress value with gzencode', 1744534319); + } + } + return $value; } private function useCompression(): bool diff --git a/Neos.Cache/Classes/Backend/RequireOnceFromValueTrait.php b/Neos.Cache/Classes/Backend/RequireOnceFromValueTrait.php index 5cc7809103..94786d597f 100644 --- a/Neos.Cache/Classes/Backend/RequireOnceFromValueTrait.php +++ b/Neos.Cache/Classes/Backend/RequireOnceFromValueTrait.php @@ -19,7 +19,7 @@ trait RequireOnceFromValueTrait { /** - * @var array + * @var array */ protected $_requiredEntryIdentifiers = []; diff --git a/Neos.Cache/Classes/Backend/SimpleFileBackend.php b/Neos.Cache/Classes/Backend/SimpleFileBackend.php index 6f3e346b0b..598ea30de9 100644 --- a/Neos.Cache/Classes/Backend/SimpleFileBackend.php +++ b/Neos.Cache/Classes/Backend/SimpleFileBackend.php @@ -82,12 +82,13 @@ class SimpleFileBackend extends IndependentAbstractBackend implements PhpCapable * the effective directory will be a subdirectory of this. * If not given this will be determined by the EnvironmentConfiguration * - * @var string + * @var ?string */ protected $baseDirectory; /** * {@inheritdoc} + * @param array $options */ public function __construct(EnvironmentConfiguration $environmentConfiguration, array $options = []) { @@ -138,7 +139,7 @@ public function getCacheDirectory(): string * * @param string $entryIdentifier An identifier for this specific cache entry * @param string $data The data to be stored - * @param array $tags Ignored in this type of cache backend + * @param array $tags Ignored in this type of cache backend * @param int|null $lifetime Ignored in this type of cache backend * @return void * @throws Exception if the directory does not exist or is not writable or exceeds the maximum allowed path length, or if no cache frontend has been set. @@ -408,6 +409,7 @@ public function valid(): bool * * @return void * @api + * @phpstan-assert \DirectoryIterator $this->cacheFilesIterator */ public function rewind(): void { @@ -427,15 +429,15 @@ public function rewind(): void */ protected function throwExceptionIfPathExceedsMaximumLength(string $cacheEntryPathAndFilename): void { - if (strlen($cacheEntryPathAndFilename) > $this->environmentConfiguration->getMaximumPathLength()) { - throw new Exception('The length of the cache entry path "' . $cacheEntryPathAndFilename . '" exceeds the maximum path length of ' . $this->environmentConfiguration->getMaximumPathLength() . '. Please consider setting the FLOW_PATH_TEMPORARY_BASE environment variable to a shorter path. ', 1248710426); + if (strlen($cacheEntryPathAndFilename) > $this->environmentConfiguration?->getMaximumPathLength()) { + throw new Exception('The length of the cache entry path "' . $cacheEntryPathAndFilename . '" exceeds the maximum path length of ' . $this->environmentConfiguration?->getMaximumPathLength() . '. Please consider setting the FLOW_PATH_TEMPORARY_BASE environment variable to a shorter path. ', 1248710426); } } /** - * @return string + * @return ?string */ - public function getBaseDirectory(): string + public function getBaseDirectory(): ?string { return $this->baseDirectory; } @@ -456,7 +458,10 @@ protected function configureCacheDirectory(): void $cacheDirectory = $this->cacheDirectory; if ($cacheDirectory === '') { $codeOrData = ($this->cache instanceof PhpFrontend) ? 'Code' : 'Data'; - $baseDirectory = ($this->baseDirectory ?: $this->environmentConfiguration->getFileCacheBasePath()); + $baseDirectory = ($this->baseDirectory ?: $this->environmentConfiguration?->getFileCacheBasePath()); + if ($baseDirectory === null) { + throw new \RuntimeException('Could not resolve base directory for SimpleFileBackend', 1744534007); + } $cacheDirectory = $baseDirectory . 'Cache/' . $codeOrData . '/' . $this->cacheIdentifier . '/'; } @@ -504,9 +509,9 @@ protected function readCacheFile(string $cacheEntryPathAndFilename, int $offset } if (flock($file, LOCK_SH) !== false) { $data = ''; - if ($maxlen !== 0) { - $data = $maxlen !== null ? file_get_contents($cacheEntryPathAndFilename, false, null, $offset, $maxlen) : file_get_contents($cacheEntryPathAndFilename, false, null, $offset); - } + $data = $maxlen > 0 + ? file_get_contents($cacheEntryPathAndFilename, false, null, $offset, $maxlen) + : file_get_contents($cacheEntryPathAndFilename, false, null, $offset); flock($file, LOCK_UN); } diff --git a/Neos.Cache/Classes/Backend/TaggableMultiBackend.php b/Neos.Cache/Classes/Backend/TaggableMultiBackend.php index aef92b8b3f..3ce1ac7aa1 100644 --- a/Neos.Cache/Classes/Backend/TaggableMultiBackend.php +++ b/Neos.Cache/Classes/Backend/TaggableMultiBackend.php @@ -23,6 +23,7 @@ class TaggableMultiBackend extends MultiBackend implements TaggableBackendInterface { /** + * @param array $backendOptions * @throws Throwable */ protected function buildSubBackend(string $backendClassName, array $backendOptions): ?BackendInterface @@ -46,6 +47,9 @@ public function flushByTag(string $tag): int $flushed = 0; foreach ($this->backends as $backend) { try { + if (!method_exists($backend, 'flushByTag')) { + throw new \RuntimeException(get_class($backend) . ' does not implement flushByTag'); + } $flushed += $backend->flushByTag($tag); } catch (Throwable $throwable) { $this->logger?->error('Failed flushing cache by tag using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); @@ -80,6 +84,9 @@ public function findIdentifiersByTag(string $tag): array $identifiers = []; foreach ($this->backends as $backend) { try { + if (!method_exists($backend, 'findIdentifiersByTag')) { + throw new \RuntimeException(get_class($backend) . ' does not implement findIdentifiersByTag'); + } $identifiers[] = $backend->findIdentifiersByTag($tag); } catch (Throwable $throwable) { $this->logger?->error('Failed finding identifiers by tag using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); diff --git a/Neos.Cache/Classes/Backend/TransientMemoryBackend.php b/Neos.Cache/Classes/Backend/TransientMemoryBackend.php index 1ea561187c..c57036cbdf 100644 --- a/Neos.Cache/Classes/Backend/TransientMemoryBackend.php +++ b/Neos.Cache/Classes/Backend/TransientMemoryBackend.php @@ -25,12 +25,12 @@ class TransientMemoryBackend extends IndependentAbstractBackend implements TaggableBackendInterface { /** - * @var array + * @var array */ protected $entries = []; /** - * @var array + * @var array> */ protected $tagsAndEntries = []; @@ -39,8 +39,8 @@ class TransientMemoryBackend extends IndependentAbstractBackend implements Tagga * * @param string $entryIdentifier An identifier for this specific cache entry * @param string $data The data to be stored - * @param array $tags Tags to associate with this cache entry - * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. + * @param array $tags Tags to associate with this cache entry + * @param ?integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. * @return void * @throws Exception if no cache frontend has been set. * @api @@ -51,7 +51,6 @@ public function set(string $entryIdentifier, string $data, array $tags = [], ?in throw new Exception('No cache frontend has been set yet via setCache().', 1238244992); } - $this->entries[$entryIdentifier] = $data; foreach ($tags as $tag) { $this->tagsAndEntries[$tag][$entryIdentifier] = true; diff --git a/Neos.Cache/Classes/BackendInstantiationTrait.php b/Neos.Cache/Classes/BackendInstantiationTrait.php index eb0c935968..42464cad84 100644 --- a/Neos.Cache/Classes/BackendInstantiationTrait.php +++ b/Neos.Cache/Classes/BackendInstantiationTrait.php @@ -21,7 +21,7 @@ trait BackendInstantiationTrait { /** * @param string $backendObjectName - * @param array $backendOptions + * @param array $backendOptions * @param EnvironmentConfiguration $environmentConfiguration * @return BackendInterface * @throws InvalidBackendException diff --git a/Neos.Cache/Classes/CacheFactory.php b/Neos.Cache/Classes/CacheFactory.php index 7ce4e225db..c37dde8006 100644 --- a/Neos.Cache/Classes/CacheFactory.php +++ b/Neos.Cache/Classes/CacheFactory.php @@ -49,7 +49,7 @@ public function __construct(EnvironmentConfiguration $environmentConfiguration) * @param string $cacheIdentifier The name / identifier of the cache to create * @param string $cacheObjectName Object name of the cache frontend * @param string $backendObjectName Object name of the cache backend - * @param array $backendOptions (optional) Array of backend options + * @param array $backendOptions (optional) Array of backend options * @return FrontendInterface The created cache frontend * @throws InvalidBackendException * @throws InvalidCacheException diff --git a/Neos.Cache/Classes/CacheFactoryInterface.php b/Neos.Cache/Classes/CacheFactoryInterface.php index 3bc72b7fef..1b85e27d74 100644 --- a/Neos.Cache/Classes/CacheFactoryInterface.php +++ b/Neos.Cache/Classes/CacheFactoryInterface.php @@ -30,7 +30,7 @@ interface CacheFactoryInterface * @param string $cacheIdentifier The name / identifier of the cache to create * @param string $cacheObjectName Object name of the cache frontend * @param string $backendObjectName Object name of the cache backend - * @param array $backendOptions (optional) Array of backend options + * @param array $backendOptions (optional) Array of backend options * @return \Neos\Cache\Frontend\FrontendInterface The created cache frontend * @throws InvalidBackendException * @throws InvalidCacheException diff --git a/Neos.Cache/Classes/Frontend/CacheEntryIterator.php b/Neos.Cache/Classes/Frontend/CacheEntryIterator.php index 45ae20e2c1..b3244a4641 100644 --- a/Neos.Cache/Classes/Frontend/CacheEntryIterator.php +++ b/Neos.Cache/Classes/Frontend/CacheEntryIterator.php @@ -18,6 +18,7 @@ /** * An iterator for cache entries * + * @implements \Iterator * @api */ class CacheEntryIterator implements \Iterator diff --git a/Neos.Cache/Classes/Frontend/FrontendInterface.php b/Neos.Cache/Classes/Frontend/FrontendInterface.php index 18fdf62b6f..b6e7a61c02 100644 --- a/Neos.Cache/Classes/Frontend/FrontendInterface.php +++ b/Neos.Cache/Classes/Frontend/FrontendInterface.php @@ -51,8 +51,8 @@ public function getBackend(); * * @param string $entryIdentifier Something which identifies the data - depends on concrete cache * @param mixed $data The data to cache - also depends on the concrete cache implementation - * @param array $tags Tags to associate with this cache entry - * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. + * @param array $tags Tags to associate with this cache entry + * @param ?integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. * @return void * @api */ @@ -71,7 +71,7 @@ public function get(string $entryIdentifier); * Finds and returns all cache entries which are tagged by the specified tag. * * @param string $tag The tag to search for - * @return array An array with the identifier (key) and content (value) of all matching entries. An empty array if no entries matched + * @return array An array with the identifier (key) and content (value) of all matching entries. An empty array if no entries matched * @api */ public function getByTag(string $tag): array; diff --git a/Neos.Cache/Classes/Frontend/PhpFrontend.php b/Neos.Cache/Classes/Frontend/PhpFrontend.php index c47efbab1f..11d442d3be 100644 --- a/Neos.Cache/Classes/Frontend/PhpFrontend.php +++ b/Neos.Cache/Classes/Frontend/PhpFrontend.php @@ -60,7 +60,7 @@ public function get(string $entryIdentifier) preg_match('/^(?:.*\n){1}((?:.*\n)*)(?:.+\n?|\n)$/', $code, $matches); - return $matches[1]; + return $matches[1] ?? false; } /** @@ -83,21 +83,22 @@ public function getWrapped(string $entryIdentifier) * Saves the PHP source code in the cache. * * @param string $entryIdentifier An identifier used for this cache entry, for example the class name - * @param string $sourceCode PHP source code - * @param array $tags Tags to associate with this cache entry - * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. + * @param string $data PHP source code + * @param array $tags Tags to associate with this cache entry + * @param ?integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. * @return void * @throws InvalidDataException * @throws \InvalidArgumentException * @throws \Neos\Cache\Exception * @api */ - public function set(string $entryIdentifier, $sourceCode, array $tags = [], ?int $lifetime = null) + public function set(string $entryIdentifier, $data, array $tags = [], ?int $lifetime = null) { if (!$this->isValidEntryIdentifier($entryIdentifier)) { throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1264023823); } - if (!is_string($sourceCode)) { + /** @phpstan-ignore function.alreadyNarrowedType (we cannot narrow this on language level) */ + if (!is_string($data)) { throw new InvalidDataException('The given source code is not a valid string.', 1264023824); } foreach ($tags as $tag) { @@ -105,7 +106,7 @@ public function set(string $entryIdentifier, $sourceCode, array $tags = [], ?int throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1264023825); } } - $sourceCode = 'backend->set($entryIdentifier, $sourceCode, $tags, $lifetime); } diff --git a/Neos.Cache/Classes/Frontend/StringFrontend.php b/Neos.Cache/Classes/Frontend/StringFrontend.php index e234b57ef7..829e83dba1 100644 --- a/Neos.Cache/Classes/Frontend/StringFrontend.php +++ b/Neos.Cache/Classes/Frontend/StringFrontend.php @@ -28,22 +28,23 @@ class StringFrontend extends AbstractFrontend * Saves the value of a PHP variable in the cache. * * @param string $entryIdentifier An identifier used for this cache entry - * @param string $string The variable to cache - * @param array $tags Tags to associate with this cache entry - * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. + * @param string $data The variable to cache + * @param array $tags Tags to associate with this cache entry + * @param ?integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. * @return void * @throws InvalidDataException * @throws \InvalidArgumentException * @throws \Neos\Cache\Exception * @api */ - public function set(string $entryIdentifier, $string, array $tags = [], ?int $lifetime = null) + public function set(string $entryIdentifier, $data, array $tags = [], ?int $lifetime = null) { if (!$this->isValidEntryIdentifier($entryIdentifier)) { throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233057566); } - if (!is_string($string)) { - throw new InvalidDataException('Given data is of type "' . gettype($string) . '", but a string is expected for string cache.', 1222808333); + /** @phpstan-ignore function.alreadyNarrowedType (we cannot narrow this on language level) */ + if (!is_string($data)) { + throw new InvalidDataException('Given data is of type "' . gettype($data) . '", but a string is expected for string cache.', 1222808333); } foreach ($tags as $tag) { if (!$this->isValidTag($tag)) { @@ -51,7 +52,7 @@ public function set(string $entryIdentifier, $string, array $tags = [], ?int $li } } - $this->backend->set($entryIdentifier, $string, $tags, $lifetime); + $this->backend->set($entryIdentifier, $data, $tags, $lifetime); } /** @@ -75,7 +76,7 @@ public function get(string $entryIdentifier) * Finds and returns all cache entries which are tagged by the specified tag. * * @param string $tag The tag to search for - * @return array An array with the identifier (key) and content (value) of all matching entries. An empty array if no entries matched + * @return array An array with the identifier (key) and content (value) of all matching entries. An empty array if no entries matched * @throws NotSupportedByBackendException * @throws \InvalidArgumentException * @api diff --git a/Neos.Cache/Classes/Frontend/VariableFrontend.php b/Neos.Cache/Classes/Frontend/VariableFrontend.php index 01602138dc..bc6d379f70 100644 --- a/Neos.Cache/Classes/Frontend/VariableFrontend.php +++ b/Neos.Cache/Classes/Frontend/VariableFrontend.php @@ -48,15 +48,15 @@ public function initializeObject() * will be serialized if necessary. * * @param string $entryIdentifier An identifier used for this cache entry - * @param mixed $variable The variable to cache - * @param array $tags Tags to associate with this cache entry - * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. + * @param mixed $data The variable to cache + * @param array $tags Tags to associate with this cache entry + * @param ?integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. * @return void * @throws \InvalidArgumentException * @throws \Neos\Cache\Exception * @api */ - public function set(string $entryIdentifier, $variable, array $tags = [], ?int $lifetime = null) + public function set(string $entryIdentifier, $data, array $tags = [], ?int $lifetime = null) { if (!$this->isValidEntryIdentifier($entryIdentifier)) { throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233058264); @@ -67,9 +67,9 @@ public function set(string $entryIdentifier, $variable, array $tags = [], ?int $ } } if ($this->useIgBinary === true) { - $this->backend->set($entryIdentifier, igbinary_serialize($variable), $tags, $lifetime); + $this->backend->set($entryIdentifier, igbinary_serialize($data) ?: '', $tags, $lifetime); } else { - $this->backend->set($entryIdentifier, serialize($variable), $tags, $lifetime); + $this->backend->set($entryIdentifier, serialize($data), $tags, $lifetime); } } @@ -98,7 +98,7 @@ public function get(string $entryIdentifier) * Finds and returns all cache entries which are tagged by the specified tag. * * @param string $tag The tag to search for - * @return array An array with the identifier (key) and content (value) of all matching entries. An empty array if no entries matched + * @return array An array with the identifier (key) and content (value) of all matching entries. An empty array if no entries matched * @throws NotSupportedByBackendException * @throws \InvalidArgumentException * @api diff --git a/Neos.Cache/Classes/Psr/Cache/CacheFactory.php b/Neos.Cache/Classes/Psr/Cache/CacheFactory.php index 9b2d73d6e9..0daeae17b1 100644 --- a/Neos.Cache/Classes/Psr/Cache/CacheFactory.php +++ b/Neos.Cache/Classes/Psr/Cache/CacheFactory.php @@ -48,7 +48,7 @@ public function __construct(EnvironmentConfiguration $environmentConfiguration) * * @param string $cacheIdentifier The name / identifier of the cache to create. * @param string $backendObjectName Object name of the cache backend - * @param array $backendOptions (optional) Array of backend options + * @param array $backendOptions (optional) Array of backend options * @return CacheItemPoolInterface * @throws InvalidBackendException */ diff --git a/Neos.Cache/Classes/Psr/Cache/CachePool.php b/Neos.Cache/Classes/Psr/Cache/CachePool.php index 998b0f4653..de83c88ebb 100644 --- a/Neos.Cache/Classes/Psr/Cache/CachePool.php +++ b/Neos.Cache/Classes/Psr/Cache/CachePool.php @@ -44,7 +44,7 @@ class CachePool implements CacheItemPoolInterface /** * A list of items still to be persisted. * - * @var array + * @var array */ protected $deferredItems = []; @@ -90,7 +90,7 @@ public function getItem(string $key): CacheItemInterface * Returns a traversable set of cache items. * * @param string[] $keys - * @return array + * @return array * @throws InvalidArgumentException */ public function getItems(array $keys = []): iterable diff --git a/Neos.Cache/Classes/Psr/SimpleCache/SimpleCache.php b/Neos.Cache/Classes/Psr/SimpleCache/SimpleCache.php index 61ee172b06..38116ad4b4 100644 --- a/Neos.Cache/Classes/Psr/SimpleCache/SimpleCache.php +++ b/Neos.Cache/Classes/Psr/SimpleCache/SimpleCache.php @@ -133,7 +133,7 @@ public function clear(): bool } /** - * @param iterable $keys + * @param iterable $keys * @param mixed $default * @return iterable * @throws InvalidArgumentException @@ -149,7 +149,7 @@ public function getMultiple(iterable $keys, mixed $default = null): iterable } /** - * @param iterable $values + * @param iterable $values * @param null|int|\DateInterval $ttl * @return bool * @throws Exception @@ -171,7 +171,7 @@ public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null } /** - * @param iterable $keys + * @param iterable $keys * @return bool * @throws InvalidArgumentException * @throws Exception @@ -180,7 +180,7 @@ public function deleteMultiple(iterable $keys): bool { foreach ($keys as $key) { $this->delete($key); - }; + } return true; } diff --git a/Neos.Cache/Classes/Psr/SimpleCache/SimpleCacheFactory.php b/Neos.Cache/Classes/Psr/SimpleCache/SimpleCacheFactory.php index 9daeb39c82..7c4e4ce6e7 100644 --- a/Neos.Cache/Classes/Psr/SimpleCache/SimpleCacheFactory.php +++ b/Neos.Cache/Classes/Psr/SimpleCache/SimpleCacheFactory.php @@ -48,7 +48,7 @@ public function __construct(EnvironmentConfiguration $environmentConfiguration) * * @param string $cacheIdentifier The name / identifier of the cache to create. * @param string $backendObjectName Object name of the cache backend - * @param array $backendOptions (optional) Array of backend options + * @param array $backendOptions (optional) Array of backend options * @return CacheInterface * @throws InvalidBackendException */ diff --git a/Neos.Eel/Classes/CompilingEvaluator.php b/Neos.Eel/Classes/CompilingEvaluator.php index f2d8200285..e322fe03b1 100644 --- a/Neos.Eel/Classes/CompilingEvaluator.php +++ b/Neos.Eel/Classes/CompilingEvaluator.php @@ -39,7 +39,7 @@ class CompilingEvaluator implements EelEvaluatorInterface * * @param StringFrontend $expressionCache */ - public function injectExpressionCache(StringFrontend $expressionCache) + public function injectExpressionCache(StringFrontend $expressionCache): void { $this->expressionCache = $expressionCache; } diff --git a/Neos.Eel/Classes/Context.php b/Neos.Eel/Classes/Context.php index c4c9cc80e1..4b916380b3 100644 --- a/Neos.Eel/Classes/Context.php +++ b/Neos.Eel/Classes/Context.php @@ -58,7 +58,7 @@ public function get($path) if (is_array($this->value)) { return array_key_exists($path, $this->value) ? $this->value[$path] : null; } elseif (is_object($this->value)) { - $this->tracer?->recordPropertyAccess($this->value, $path); + $this->tracer?->recordPropertyAccess($this->value, (string)$path); try { return ObjectAccess::getProperty($this->value, $path); } catch (PropertyNotAccessibleException $exception) { @@ -73,19 +73,19 @@ public function get($path) /** * Get a value by path and wrap it into another context * - * @param string $path + * @param ?string $path * @return Context The wrapped value */ public function getAndWrap($path = null) { - return $this->wrap($this->get($path)); + return $this->wrap($this->get($path ?: '') ?: ''); } /** * Call a method on this context * * @param string $method - * @param array $arguments Arguments to the method, if of type Context they will be unwrapped + * @param array $arguments Arguments to the method, if of type Context they will be unwrapped * @return mixed * @throws \Exception */ @@ -127,7 +127,7 @@ public function call($method, array $arguments = []) * Call a method and wrap the result * * @param string $method - * @param array $arguments + * @param array $arguments * @return mixed */ public function callAndWrap($method, array $arguments = []) diff --git a/Neos.Eel/Classes/FlowQuery/FlowQuery.php b/Neos.Eel/Classes/FlowQuery/FlowQuery.php index a78491bc6f..f7dfa4a2f4 100644 --- a/Neos.Eel/Classes/FlowQuery/FlowQuery.php +++ b/Neos.Eel/Classes/FlowQuery/FlowQuery.php @@ -71,13 +71,17 @@ * If an operation is final, it should return the resulting value directly. * * @phpstan-consistent-constructor + * + * @phpstan-type Operation array{name: string, arguments: array} + * + * @implements \IteratorAggregate */ class FlowQuery implements ProtectedContextAwareInterface, \IteratorAggregate, \Countable { /** * the objects this FlowQuery object wraps * - * @var array|\Traversable + * @var array|\Traversable */ protected $context; @@ -87,7 +91,7 @@ class FlowQuery implements ProtectedContextAwareInterface, \IteratorAggregate, \ * whereas the name is a string like 'children' and the arguments * are a numerically-indexed array * - * @var array + * @var array */ protected $operations = []; @@ -104,16 +108,12 @@ class FlowQuery implements ProtectedContextAwareInterface, \IteratorAggregate, \ * * If a FlowQuery is given as the $context we unwrap its context to assert q(q(context)) == q(context). * - * @param array|\Traversable $context The initial context (wrapped objects) for this FlowQuery - * @param array $operations - * @throws Exception + * @param array|\Traversable $context The initial context (wrapped objects) for this FlowQuery + * @param array $operations * @api */ public function __construct($context, array $operations = []) { - if (!(is_array($context) || $context instanceof \Traversable)) { - throw new Exception('The FlowQuery context must be an array or implement \Traversable but context was a ' . gettype($context), 1380816689); - } if ($context instanceof FlowQuery) { $this->context = $context->getContext(); } else { @@ -139,7 +139,7 @@ public static function q($element): self * * @param OperationResolverInterface $operationResolver */ - public function setOperationResolver(OperationResolverInterface $operationResolver) + public function setOperationResolver(OperationResolverInterface $operationResolver): void { $this->operationResolver = $operationResolver; } @@ -150,7 +150,7 @@ public function setOperationResolver(OperationResolverInterface $operationResolv * If the operation is final, we directly compute the result and return the value. * * @param string $operationName - * @param array $arguments + * @param array $arguments * @return FlowQuery|mixed */ public function __call($operationName, array $arguments) @@ -194,14 +194,14 @@ public function count(): int * * Should NEVER be called inside an operation! * - * @return \ArrayIterator + * @return \ArrayIterator */ public function getIterator(): \ArrayIterator { if (count($this->operations) > 0) { $this->evaluateOperations(); } - return new \ArrayIterator($this->context); + return new \ArrayIterator($this->context instanceof \Traversable ? iterator_to_array($this->context) : $this->context); } /** @@ -227,7 +227,7 @@ protected function evaluateOperations() * * Should only be called inside an operation. * - * @return array + * @return Operation|null */ public function popOperation() { @@ -242,9 +242,9 @@ public function popOperation() * Should only be called inside an operation. * * @param string $operationName - * @param array $arguments + * @param array $arguments */ - public function pushOperation($operationName, array $arguments) + public function pushOperation($operationName, array $arguments): void { array_unshift($this->operations, [ 'name' => $operationName, @@ -273,7 +273,7 @@ public function peekOperationName() * * Should only be called inside an operation. * - * @return array|\Traversable + * @return array|\Traversable */ public function getContext() { @@ -285,9 +285,9 @@ public function getContext() * * Should only be called inside an operation. * - * @param array|\Traversable $context + * @param array|\Traversable $context */ - public function setContext($context) + public function setContext($context): void { $this->context = $context; } diff --git a/Neos.Eel/Classes/FlowQuery/OperationInterface.php b/Neos.Eel/Classes/FlowQuery/OperationInterface.php index fed593f28f..981e7c1260 100644 --- a/Neos.Eel/Classes/FlowQuery/OperationInterface.php +++ b/Neos.Eel/Classes/FlowQuery/OperationInterface.php @@ -42,7 +42,7 @@ public static function isFinal(); * can work with the $context objects. It can be implemented * to implement runtime conditions. * - * @param array $context (or array-like object) $context onto which this operation should be applied + * @param array|\Traversable $context (or array-like object) $context onto which this operation should be applied * @return boolean true if the operation can be applied onto the $context, false otherwise * @api */ @@ -57,7 +57,7 @@ public function canEvaluate($context); * If the operation is final, evaluate should directly return the operation result. * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments the arguments for this operation + * @param array $arguments the arguments for this operation * @return mixed if the operation is final, the return value */ public function evaluate(FlowQuery $flowQuery, array $arguments); diff --git a/Neos.Eel/Classes/FlowQuery/OperationResolver.php b/Neos.Eel/Classes/FlowQuery/OperationResolver.php index ce8685de32..b61c0d969e 100644 --- a/Neos.Eel/Classes/FlowQuery/OperationResolver.php +++ b/Neos.Eel/Classes/FlowQuery/OperationResolver.php @@ -38,7 +38,7 @@ class OperationResolver implements OperationResolverInterface * 2-dimensional array of registered operations: * shortOperationName => priority => operation class name * - * @var array + * @var array> */ protected $operations = []; @@ -46,14 +46,14 @@ class OperationResolver implements OperationResolverInterface * associative array of registered final operations: * shortOperationName => shortOperationName * - * @var array + * @var array */ protected $finalOperationNames = []; /** * Initializer, building up $this->operations and $this->finalOperationNames */ - public function initializeObject() + public function initializeObject(): void { $operationsAndFinalOperationNames = static::buildOperationsAndFinalOperationNames($this->objectManager); $this->operations = $operationsAndFinalOperationNames[0]; @@ -62,7 +62,7 @@ public function initializeObject() /** * @param ObjectManagerInterface $objectManager - * @return array Array of sorted operations and array of final operation names + * @return array{0: array, 1: array} Array of sorted operations and array of final operation names * @throws FlowQueryException * @Flow\CompileStatic */ diff --git a/Neos.Eel/Classes/FlowQuery/Operations/AbstractOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/AbstractOperation.php index a2527ce36f..0a9e2a3f43 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/AbstractOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/AbstractOperation.php @@ -80,7 +80,7 @@ public static function getShortName() /** * {@inheritdoc} * - * @param array $context (or array-like object) $context onto which this operation should be applied + * @param array $context (or array-like object) $context onto which this operation should be applied * @return boolean true if the operation can be applied onto the $context, false otherwise * @api */ diff --git a/Neos.Eel/Classes/FlowQuery/Operations/AddOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/AddOperation.php index de8f2e0d44..535ad4b64c 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/AddOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/AddOperation.php @@ -31,7 +31,7 @@ class AddOperation extends AbstractOperation * {@inheritdoc} * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments the elements to add (as array in index 0) + * @param array $arguments the elements to add (as array in index 0) * @return void */ public function evaluate(FlowQuery $flowQuery, array $arguments) diff --git a/Neos.Eel/Classes/FlowQuery/Operations/CountOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/CountOperation.php index 5626d58242..a74f285a43 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/CountOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/CountOperation.php @@ -38,13 +38,15 @@ class CountOperation extends AbstractOperation * {@inheritdoc} * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments filter arguments for this operation + * @param array $arguments filter arguments for this operation * @return void|integer with the number of elements */ public function evaluate(FlowQuery $flowQuery, array $arguments) { if (count($arguments) == 0) { - return count($flowQuery->getContext()); + $context = $flowQuery->getContext(); + $context = $context instanceof \Traversable ? iterator_to_array($context) : $context; + return count($context); } else { $flowQuery->pushOperation('count', []); $flowQuery->pushOperation('filter', $arguments); diff --git a/Neos.Eel/Classes/FlowQuery/Operations/FirstOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/FirstOperation.php index 2f7e9f6685..4686e010ff 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/FirstOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/FirstOperation.php @@ -29,12 +29,13 @@ class FirstOperation extends AbstractOperation * {@inheritdoc} * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments Ignored for this operation + * @param array $arguments Ignored for this operation * @return void */ public function evaluate(FlowQuery $flowQuery, array $arguments) { $context = $flowQuery->getContext(); + $context = $context instanceof \Traversable ? iterator_to_array($context) : $context; if (isset($context[0])) { $flowQuery->setContext([$context[0]]); } else { diff --git a/Neos.Eel/Classes/FlowQuery/Operations/GetOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/GetOperation.php index d3404b9463..a116d071a7 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/GetOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/GetOperation.php @@ -44,12 +44,13 @@ class GetOperation extends AbstractOperation * {@inheritdoc} * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments the context index to fetch from + * @param array $arguments the context index to fetch from * @return mixed */ public function evaluate(FlowQuery $flowQuery, array $arguments) { $context = $flowQuery->getContext(); + $context = $context instanceof \Traversable ? iterator_to_array($context) : $context; if (isset($arguments[0])) { $index = $arguments[0]; if (isset($context[$index])) { diff --git a/Neos.Eel/Classes/FlowQuery/Operations/IsOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/IsOperation.php index 336af81685..bf828d1b28 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/IsOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/IsOperation.php @@ -39,13 +39,15 @@ class IsOperation extends AbstractOperation * {@inheritdoc} * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments the filter arguments + * @param array $arguments the filter arguments * @return mixed */ public function evaluate(FlowQuery $flowQuery, array $arguments) { if (count($arguments) == 0) { - return count($flowQuery->getContext()) > 0; + $context = $flowQuery->getContext(); + $context = $context instanceof \Traversable ? iterator_to_array($context) : $context; + return count($context) > 0; } else { $flowQuery->pushOperation('is', []); $flowQuery->pushOperation('filter', $arguments); diff --git a/Neos.Eel/Classes/FlowQuery/Operations/LastOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/LastOperation.php index f5cfd87b5a..784202cc6f 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/LastOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/LastOperation.php @@ -29,12 +29,13 @@ class LastOperation extends AbstractOperation * {@inheritdoc} * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments Ignored for this operation + * @param array $arguments Ignored for this operation * @return void */ public function evaluate(FlowQuery $flowQuery, array $arguments) { $context = $flowQuery->getContext(); + $context = $context instanceof \Traversable ? iterator_to_array($context) : $context; if (count($context) > 0) { $flowQuery->setContext([end($context)]); } else { diff --git a/Neos.Eel/Classes/FlowQuery/Operations/Object/ChildrenOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/Object/ChildrenOperation.php index 8517220949..c73509e2be 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/Object/ChildrenOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/Object/ChildrenOperation.php @@ -36,19 +36,24 @@ class ChildrenOperation extends AbstractOperation * {@inheritdoc} * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments the filter expression to use (in index 0) + * @param array $arguments the filter expression to use (in index 0) * @return void * @throws FizzleException */ public function evaluate(FlowQuery $flowQuery, array $arguments) { - if (count($flowQuery->getContext()) === 0) { + $context = $flowQuery->getContext(); + $context = $context instanceof \Traversable ? iterator_to_array($context) : $context; + if (count($context) === 0) { return; } if (!isset($arguments[0]) || empty($arguments[0])) { if ($flowQuery->peekOperationName() === 'filter') { $filterOperation = $flowQuery->popOperation(); + if ($filterOperation === null) { + throw new FizzleException('Could not resolve filter operation', 1744532569); + } if (count($filterOperation['arguments']) === 0 || empty($filterOperation['arguments'][0])) { throw new FizzleException('Filter() needs arguments if it follows an empty children(): children().filter()', 1332489382); } diff --git a/Neos.Eel/Classes/FlowQuery/Operations/Object/FilterOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/Object/FilterOperation.php index fe4ccee930..14112c0a1e 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/Object/FilterOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/Object/FilterOperation.php @@ -87,7 +87,7 @@ class FilterOperation extends AbstractOperation * {@inheritdoc} * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments the filter expression to use (in index 0) + * @param array $arguments the filter expression to use (in index 0) * @return void * @throws FizzleException */ @@ -120,7 +120,7 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) * Filter Group is something like "[foo], [bar]" * * @param object $element - * @param array $parsedFilter + * @param array $parsedFilter * @return boolean true if $element matches filter group, false otherwise */ protected function matchesFilterGroup($element, array $parsedFilter) @@ -138,7 +138,7 @@ protected function matchesFilterGroup($element, array $parsedFilter) * Match a single filter, i.e. [foo]. It matches only if all filter parts match. * * @param object $element - * @param string $filter + * @param array $filter * @return boolean true if $element matches filter, false otherwise */ protected function matchesFilter($element, $filter) @@ -177,7 +177,7 @@ protected function matchesPropertyNameFilter($element, $propertyNameFilter) * Match a single attribute filter * * @param mixed $element - * @param array $attributeFilter + * @param array $attributeFilter * @return boolean */ protected function matchesAttributeFilter($element, array $attributeFilter) diff --git a/Neos.Eel/Classes/FlowQuery/Operations/Object/PropertyOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/Object/PropertyOperation.php index d3d0b1ac41..8b598d2078 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/Object/PropertyOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/Object/PropertyOperation.php @@ -43,7 +43,7 @@ class PropertyOperation extends AbstractOperation * {@inheritdoc} * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments the property path to use (in index 0) + * @param array $arguments the property path to use (in index 0) * @return mixed */ public function evaluate(FlowQuery $flowQuery, array $arguments) @@ -53,6 +53,7 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) } $context = $flowQuery->getContext(); + $context = $context instanceof \Traversable ? iterator_to_array($context) : $context; if (!isset($context[0])) { return null; } diff --git a/Neos.Eel/Classes/FlowQuery/Operations/RemoveOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/RemoveOperation.php index 7999975bc3..c05dfcec0f 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/RemoveOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/RemoveOperation.php @@ -31,7 +31,7 @@ class RemoveOperation extends AbstractOperation * {@inheritdoc} * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments the elements to remove (as array in index 0) + * @param array $arguments the elements to remove (as array in index 0) * @return void */ public function evaluate(FlowQuery $flowQuery, array $arguments) @@ -46,8 +46,9 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) $valuesToRemove[] = $arguments[0]; } } + $context = $flowQuery->getContext(); $filteredContext = array_filter( - $flowQuery->getContext(), + $context instanceof \Traversable ? iterator_to_array($context) : $context, function ($item) use ($valuesToRemove) { return in_array($item, $valuesToRemove, true) === false; } diff --git a/Neos.Eel/Classes/FlowQuery/Operations/SliceOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/SliceOperation.php index 16201f6de3..c5d71c7e8e 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/SliceOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/SliceOperation.php @@ -32,13 +32,13 @@ class SliceOperation extends AbstractOperation * {@inheritdoc} * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments A mandatory start and optional end index in the context, negative indices indicate an offset from the start or end respectively + * @param array $arguments A mandatory start and optional end index in the context, negative indices indicate an offset from the start or end respectively * @return void */ public function evaluate(FlowQuery $flowQuery, array $arguments) { $context = $flowQuery->getContext(); - if ($context instanceof \Iterator) { + if ($context instanceof \Traversable) { /** @todo fix me in 8.3+ */ $context = iterator_to_array($context); } if (isset($arguments[0]) && isset($arguments[1])) { diff --git a/Neos.Eel/Classes/FlowQuery/Operations/UniqueOperation.php b/Neos.Eel/Classes/FlowQuery/Operations/UniqueOperation.php index e849455cd1..20dbd0f3f6 100644 --- a/Neos.Eel/Classes/FlowQuery/Operations/UniqueOperation.php +++ b/Neos.Eel/Classes/FlowQuery/Operations/UniqueOperation.php @@ -29,7 +29,7 @@ class UniqueOperation extends AbstractOperation * {@inheritdoc} * * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments the elements to remove (as array in index 0) + * @param array $arguments the elements to remove (as array in index 0) * @return void */ public function evaluate(FlowQuery $flowQuery, array $arguments) diff --git a/Neos.Eel/Classes/Helper/ArrayHelper.php b/Neos.Eel/Classes/Helper/ArrayHelper.php index ad7936a75b..d979839372 100644 --- a/Neos.Eel/Classes/Helper/ArrayHelper.php +++ b/Neos.Eel/Classes/Helper/ArrayHelper.php @@ -33,7 +33,7 @@ class ArrayHelper implements ProtectedContextAwareInterface * @param iterable|mixed $array1 First array or value * @param iterable|mixed $array2 Second array or value * @param iterable|mixed $array_ Optional variable list of additional arrays / values - * @return array The array with concatenated arrays or values + * @return array The array with concatenated arrays or values */ public function concat($array1, $array2, $array_ = null): array { @@ -51,7 +51,7 @@ public function concat($array1, $array2, $array_ = null): array /** * Join values of an array with a separator * - * @param iterable $array Array with values to join + * @param iterable $array Array with values to join * @param string $separator A separator for the values * @return string A string with the joined values separated by the separator */ @@ -66,10 +66,10 @@ public function join(iterable $array, $separator = ','): string /** * Extract a portion of an indexed array * - * @param iterable $array The array (with numeric indices) + * @param iterable $array The array (with numeric indices) * @param int $begin * @param int $end - * @return array + * @return array */ public function slice(iterable $array, $begin, $end = null): array { @@ -88,8 +88,8 @@ public function slice(iterable $array, $begin, $end = null): array /** * Returns an array in reverse order * - * @param iterable $array The array - * @return array + * @param iterable $array The array + * @return array */ public function reverse(iterable $array): array { @@ -102,8 +102,8 @@ public function reverse(iterable $array): array /** * Get the array keys * - * @param iterable $array The array - * @return array + * @param iterable $array The array + * @return array */ public function keys(iterable $array): array { @@ -116,8 +116,8 @@ public function keys(iterable $array): array /** * Get the array values * - * @param iterable $array The array - * @return array + * @param iterable $array The array + * @return array */ public function values(iterable $array): array { @@ -130,7 +130,7 @@ public function values(iterable $array): array /** * Get the length of an array * - * @param iterable $array The array + * @param iterable $array The array * @return int */ public function length(iterable $array): int @@ -144,7 +144,7 @@ public function length(iterable $array): int /** * Check if an array is empty * - * @param iterable $array The array + * @param iterable $array The array * @return bool true if the array is empty */ public function isEmpty(iterable $array): bool @@ -155,7 +155,7 @@ public function isEmpty(iterable $array): bool /** * Get the first element of an array * - * @param iterable $array The array + * @param iterable $array The array * @return mixed */ public function first(iterable $array) @@ -172,7 +172,7 @@ public function first(iterable $array) /** * Get the last element of an array * - * @param iterable $array The array + * @param iterable $array The array * @return mixed */ public function last(iterable $array) @@ -191,7 +191,7 @@ public function last(iterable $array) * Returns the first index at which a given element can be found in the array, * or -1 if it is not present * - * @param iterable $array The array + * @param iterable $array The array * @param mixed $searchElement The element value to find * @param int $fromIndex Position in the array to start the search. * @return int @@ -217,7 +217,7 @@ public function indexOf(iterable $array, $searchElement, $fromIndex = null): int /** * Picks a random element from the array * - * @param array $array + * @param array $array * @return mixed A random entry or null if the array is empty */ public function random(iterable $array) @@ -240,8 +240,8 @@ public function random(iterable $array) * Internally natsort() is used as it most closely resembles javascript's sort(). * Because there are no real associative arrays in Javascript, keys of the array will be preserved. * - * @param iterable $array - * @return array The sorted array + * @param iterable $array + * @return array The sorted array */ public function sort(iterable $array): array { @@ -268,8 +268,8 @@ public function sort(iterable $array): array /** * Sort an array by key * - * @param iterable $array The array to sort - * @return array The sorted array + * @param iterable $array The array to sort + * @return array The sorted array */ public function ksort(iterable $array): array { @@ -286,9 +286,9 @@ public function ksort(iterable $array): array * Randomizes entries an array with the option to preserve the existing keys. * When this option is set to false, all keys will be replaced * - * @param iterable $array - * @param bool $preserveKeys Wether to preserve the keys when shuffling the array - * @return array The shuffled array + * @param iterable $array + * @param bool $preserveKeys Whether to preserve the keys when shuffling the array + * @return array The shuffled array */ public function shuffle(iterable $array, $preserveKeys = true): array { @@ -315,8 +315,8 @@ public function shuffle(iterable $array, $preserveKeys = true): array /** * Removes duplicate values from an array * - * @param iterable $array The input array - * @return array The filtered array. + * @param iterable $array The input array + * @return array The filtered array. */ public function unique(iterable $array): array { @@ -333,8 +333,8 @@ public function unique(iterable $array): array * * An empty array will result in an empty array again. * - * @param iterable $array - * @return array The array without the last element + * @param iterable $array + * @return array The array without the last element */ public function pop(iterable $array): array { @@ -355,9 +355,9 @@ public function pop(iterable $array): array * * Array.push(array, e1, e2) * - * @param iterable|scalar|null $array + * @param mixed $array * @param mixed $element - * @return array The array with the inserted elements + * @return array The array with the inserted elements */ public function push($array, $element): array { @@ -388,8 +388,8 @@ public function push($array, $element): array * * An empty array will result in an empty array again. * - * @param iterable $array - * @return array The array without the first element + * @param iterable $array + * @return array The array without the first element */ public function shift(iterable $array): array { @@ -407,9 +407,9 @@ public function shift(iterable $array): array * * Array.unshift(array, e1, e2) * - * @param iterable $array + * @param iterable $array * @param mixed $element - * @return array The array with the inserted elements + * @return array The array with the inserted elements */ public function unshift(iterable $array, $element): array { @@ -432,11 +432,11 @@ public function unshift(iterable $array, $element): array * * Array.splice(array, 3, 2, 'a', 'b') * - * @param iterable $array + * @param iterable $array * @param int $offset Index of the first element to remove * @param int $length Number of elements to remove * @param mixed $replacements Elements to insert instead of the removed range - * @return array The array with removed and replaced elements + * @return array The array with removed and replaced elements */ public function splice(iterable $array, $offset, $length = 1, $replacements = null): array { @@ -455,8 +455,8 @@ public function splice(iterable $array, $offset, $length = 1, $replacements = nu * Note that the values of array need to be valid keys, i.e. they need to be either int or string. * If a value has several occurrences, the latest key will be used as its value, and all others will be lost. * - * @param iterable $array - * @return array The array with flipped keys and values + * @param iterable $array + * @return array The array with flipped keys and values */ public function flip(iterable $array): array { @@ -475,7 +475,7 @@ public function flip(iterable $array): array * @param mixed $start First value of the sequence. * @param mixed $end The sequence is ended upon reaching the end value. * @param int $step The increment between items, will default to 1. - * @return array Array of elements from start to end, inclusive. + * @return array Array of elements from start to end, inclusive. */ public function range($start, $end, $step = 1): array { @@ -485,10 +485,10 @@ public function range($start, $end, $step = 1): array /** * Set the specified key in the the array * - * @param iterable $array + * @param iterable $array * @param string|integer $key the key that should be set * @param mixed $value the value to assign to the key - * @return array The modified array. + * @return array The modified array. */ public function set(iterable $array, $key, $value): array { @@ -507,9 +507,9 @@ public function set(iterable $array, $key, $value): array * Array.map([1, 2, 3, 4], x => x * x) * Array.map([1, 2, 3, 4], (x, index) => x * index) * - * @param iterable $array Array of elements to map + * @param iterable $array Array of elements to map * @param callable $callback Callback to apply for each element, current value and key will be passed as arguments - * @return array The array with callback applied, keys will be preserved + * @return array The array with callback applied, keys will be preserved */ public function map(iterable $array, callable $callback): array { @@ -528,7 +528,7 @@ public function map(iterable $array, callable $callback): array * Array.reduce([1, 2, 3, 4], (accumulator, currentValue) => accumulator + currentValue) // == 10 * Array.reduce([1, 2, 3, 4], (accumulator, currentValue) => accumulator + currentValue, 1) // == 11 * - * @param iterable $array Array of elements to reduce to a value + * @param iterable $array Array of elements to reduce to a value * @param callable $callback Callback for accumulating values, accumulator, current value and key will be passed as arguments * @param mixed $initialValue Initial value, defaults to first item in array and callback starts with second entry * @return mixed @@ -558,9 +558,9 @@ public function reduce(iterable $array, callable $callback, $initialValue = null * Array.filter([1, 2, 3, 4], x => x % 2 == 0) // == [2, 4] * Array.filter(['foo', 'bar', 'baz'], (x, index) => index < 2) // == ['foo', 'bar'] * - * @param iterable $array Array of elements to filter - * @param callable $callback Callback for testing if an element should be included in the result, current value and key will be passed as arguments - * @return array The array with elements where callback returned true + * @param iterable $array Array of elements to filter + * @param ?callable $callback Callback for testing if an element should be included in the result, current value and key will be passed as arguments + * @return array The array with elements where callback returned true */ public function filter(iterable $array, ?callable $callback = null): array { @@ -583,7 +583,7 @@ public function filter(iterable $array, ?callable $callback = null): array * Array.some([1, 2, 3, 4], x => x % 2 == 0) // == true * Array.some([1, 2, 3, 4], x => x > 4) // == false * - * @param iterable $array Array of elements to test + * @param iterable $array Array of elements to test * @param callable $callback Callback for testing elements, current value and key will be passed as arguments * @return bool True if at least one element passed the test */ @@ -606,7 +606,7 @@ public function some(iterable $array, callable $callback): bool * Array.every([1, 2, 3, 4], x => x % 2 == 0) // == false * Array.every([2, 4, 6, 8], x => x % 2) // == true * - * @param iterable $array Array of elements to test + * @param iterable $array Array of elements to test * @param callable $callback Callback for testing elements, current value and key will be passed as arguments * @return bool True if all elements passed the test */ diff --git a/Neos.Eel/Classes/Helper/DateHelper.php b/Neos.Eel/Classes/Helper/DateHelper.php index 8d9bec997c..76e3423a91 100644 --- a/Neos.Eel/Classes/Helper/DateHelper.php +++ b/Neos.Eel/Classes/Helper/DateHelper.php @@ -41,7 +41,7 @@ class DateHelper implements ProtectedContextAwareInterface * * @param string $string * @param string $format - * @return \DateTime + * @return \DateTime|false */ public function parse($string, $format) { diff --git a/Neos.Eel/Classes/Helper/FileHelper.php b/Neos.Eel/Classes/Helper/FileHelper.php index 5da04592ce..debac9393b 100644 --- a/Neos.Eel/Classes/Helper/FileHelper.php +++ b/Neos.Eel/Classes/Helper/FileHelper.php @@ -27,7 +27,7 @@ class FileHelper implements ProtectedContextAwareInterface */ public function readFile(string $filepath): string { - return file_get_contents($filepath); + return file_get_contents($filepath) ?: ''; } /** @@ -36,14 +36,14 @@ public function readFile(string $filepath): string */ public function getSha1(string $filepath): string { - return sha1_file($filepath); + return sha1_file($filepath) ?: ''; } /** * Get file name and path information * * @param string $filepath - * @return array with keys dirname, basename, extension (if any), and filename + * @return array with keys dirname, basename, extension (if any), and filename */ public function fileInfo(string $filepath) { @@ -54,11 +54,11 @@ public function fileInfo(string $filepath) * Get file information like creation and modification times as well as size. * * @param string $filepath - * @return array with keys mode, uid, gid, size, atime, mtime, ctime, (blksize, blocks, dev, ino, nlink, rdev) + * @return array with keys mode, uid, gid, size, atime, mtime, ctime, (blksize, blocks, dev, ino, nlink, rdev) */ public function stat(string $filepath) { - return stat($filepath); + return stat($filepath) ?: []; } /** @@ -71,7 +71,7 @@ public function exists(string $filepath): bool { return file_exists($filepath); } - + /** * @param string $methodName * @return bool diff --git a/Neos.Eel/Classes/Helper/JsonHelper.php b/Neos.Eel/Classes/Helper/JsonHelper.php index 1198033e1d..d95ff70994 100644 --- a/Neos.Eel/Classes/Helper/JsonHelper.php +++ b/Neos.Eel/Classes/Helper/JsonHelper.php @@ -29,13 +29,14 @@ class JsonHelper implements ProtectedContextAwareInterface * Json.stringify(value, ['JSON_UNESCAPED_UNICODE', 'JSON_FORCE_OBJECT']) * * @param mixed $value - * @param array $options Array of option constant names as strings + * @param array $options Array of option constant names as strings * @return string */ public function stringify($value, array $options = []): string { + /** @phpstan-ignore argument.type ("constant" is callable) */ $optionSum = array_sum(array_map('constant', $options)); - return json_encode($value, $optionSum); + return json_encode($value, $optionSum | JSON_THROW_ON_ERROR); } /** diff --git a/Neos.Eel/Classes/Helper/MathHelper.php b/Neos.Eel/Classes/Helper/MathHelper.php index 1d6de50619..a659aa7f88 100644 --- a/Neos.Eel/Classes/Helper/MathHelper.php +++ b/Neos.Eel/Classes/Helper/MathHelper.php @@ -91,7 +91,7 @@ public function getSQRT2() } /** - * @param float $x A number + * @param mixed $x A number * @return float The absolute value of the given value */ public function abs($x = NAN) @@ -243,7 +243,7 @@ public function floor($x) */ public function isFinite($x) { - return is_numeric($x) && is_finite($x); + return is_numeric($x) && is_finite((float)$x); } /** @@ -256,7 +256,7 @@ public function isFinite($x) */ public function isInfinite($x) { - return is_numeric($x) && is_infinite($x); + return is_numeric($x) && is_infinite((float)$x); } /** @@ -269,7 +269,7 @@ public function isInfinite($x) */ public function isNaN($x) { - return !is_numeric($x) || is_nan($x); + return !is_numeric($x) || is_nan((float)$x); } /** @@ -402,7 +402,7 @@ public function randomInt($min, $max) * Negative values are also supported (-1 rounds to full 10ths). * * @param mixed $subject The value to round - * @param integer $precision The precision (digits after decimal point) to use, defaults to 0 + * @param mixed $precision The precision (digits after decimal point) to use, defaults to 0 * @return float The rounded value */ public function round($subject, $precision = 0) @@ -411,7 +411,7 @@ public function round($subject, $precision = 0) return NAN; } $subject = (float)$subject; - if ($precision !== null && !is_int($precision)) { + if (!is_int($precision)) { return NAN; } return round($subject, (int)$precision); @@ -420,7 +420,7 @@ public function round($subject, $precision = 0) /** * Get the sign of the given number, indicating whether the number is positive, negative or zero * - * @param integer|float $x The value + * @param mixed $x The value * @return integer|float -1, 0, 1 depending on the sign or NAN if the given value was not numeric */ public function sign($x) diff --git a/Neos.Eel/Classes/Helper/SecurityHelper.php b/Neos.Eel/Classes/Helper/SecurityHelper.php index 94400c3d14..8d75cd1473 100644 --- a/Neos.Eel/Classes/Helper/SecurityHelper.php +++ b/Neos.Eel/Classes/Helper/SecurityHelper.php @@ -80,7 +80,7 @@ public function isAuthenticated(): bool * Returns true, if access to the given privilege-target is granted * * @param string $privilegeTarget The identifier of the privilege target to decide on - * @param array $parameters Optional array of privilege parameters (simple key => value array) + * @param array $parameters Optional array of privilege parameters (simple key => value array) * @return boolean true if access is granted, false otherwise */ public function hasAccess(string $privilegeTarget, array $parameters = []): bool diff --git a/Neos.Eel/Classes/Helper/StringHelper.php b/Neos.Eel/Classes/Helper/StringHelper.php index 79b28dc035..f9ca953db7 100755 --- a/Neos.Eel/Classes/Helper/StringHelper.php +++ b/Neos.Eel/Classes/Helper/StringHelper.php @@ -230,7 +230,7 @@ public function lastIndexOf($string, $search, $toIndex = null) * * @param string $string The input string * @param string $pattern A PREG pattern - * @return array|null The matches as array or NULL if not matched + * @return array|null The matches as array or NULL if not matched * @throws EvaluationException */ public function pregMatch($string, $pattern) @@ -255,7 +255,7 @@ public function pregMatch($string, $pattern) * * @param string $string The input string * @param string $pattern A PREG pattern - * @return array|null The matches as array or NULL if not matched + * @return array|null The matches as array or NULL if not matched * @throws EvaluationException */ public function pregMatchAll($string, $pattern) @@ -287,10 +287,7 @@ public function pregMatchAll($string, $pattern) */ public function pregReplace($string, $pattern, $replace, $limit = -1) { - if ($limit === null) { - $limit = -1; - } - return preg_replace($pattern, $replace, (string)$string, $limit); + return preg_replace($pattern, $replace, (string)$string, $limit) ?: ''; } /** @@ -304,11 +301,11 @@ public function pregReplace($string, $pattern, $replace, $limit = -1) * @param string $string The input string * @param string $pattern A PREG pattern * @param integer $limit The maximum amount of items to return, in contrast to split() this will return all remaining characters in the last item (see example) - * @return array An array of the splitted parts, excluding the matched pattern + * @return array An array of the splitted parts, excluding the matched pattern */ public function pregSplit($string, $pattern, $limit = -1) { - return preg_split($pattern, (string)$string, (int)$limit); + return preg_split($pattern, (string)$string, (int)$limit) ?: []; } /** @@ -321,10 +318,10 @@ public function pregSplit($string, $pattern, $limit = -1) * * Note: this method does not perform regular expression matching, @see pregReplace(). * - * @param array|string|null $string The input string - * @param array|string|null $search A search string - * @param array|string|null $replace A replacement string - * @return array|string|string[] The string with all occurrences replaced + * @param array|string|null $string The input string + * @param array|string|null $search A search string + * @param array|string|null $replace A replacement string + * @return array|string|string[] The string with all occurrences replaced */ public function replace($string, $search, $replace) { @@ -349,7 +346,7 @@ public function replace($string, $search, $replace) * @param string $string The string to split * @param string|null $separator The separator where the string should be splitted * @param int|null $limit The maximum amount of items to split (exceeding items will be discarded) - * @return array An array of the splitted parts, excluding the separators + * @return array An array of the split parts, excluding the separators */ public function split($string, $separator = null, $limit = null) { @@ -591,7 +588,7 @@ public function rawUrlDecode($string) */ public function htmlSpecialChars($string, $preserveEntities = false) { - return htmlspecialchars((string)$string, ENT_NOQUOTES | ENT_HTML401, ini_get("default_charset"), !$preserveEntities); + return htmlspecialchars((string)$string, ENT_NOQUOTES | ENT_HTML401, ini_get("default_charset") ?: null, !$preserveEntities); } /** @@ -711,9 +708,9 @@ public function wordCount($unicodeString) { $unicodeString = (string)$unicodeString; - $unicodeString = preg_replace('/[[:punct:][:digit:]]/', '', $unicodeString); + $unicodeString = preg_replace('/[[:punct:][:digit:]]/', '', $unicodeString) ?: ''; - return count(preg_split('/[[:space:]]+/', $unicodeString, 0, PREG_SPLIT_NO_EMPTY)); + return count(preg_split('/[[:space:]]+/', $unicodeString, 0, PREG_SPLIT_NO_EMPTY) ?: []); } /** @@ -746,7 +743,7 @@ public function base64decode($string, bool $strict = false) * @see https://php.net/manual/en/function.vsprintf.php * * @param string $format A formatting string containing directives - * @param array $args An array of values to be inserted according to the formatting string $format + * @param array $args An array of values to be inserted according to the formatting string $format * @return string A string produced according to the formatting string $format */ public function format($format, array $args) diff --git a/Neos.Eel/Classes/Helper/TypeHelper.php b/Neos.Eel/Classes/Helper/TypeHelper.php index f15f370e28..9f21514ccd 100644 --- a/Neos.Eel/Classes/Helper/TypeHelper.php +++ b/Neos.Eel/Classes/Helper/TypeHelper.php @@ -47,8 +47,8 @@ public function getType($variable) /** * Get the class name of the given variable or NULL if it wasn't an object * - * @param object $variable - * @return string|NULL + * @param mixed $variable + * @return class-string|NULL */ public function className($variable) { diff --git a/Neos.Eel/Classes/ProtectedContext.php b/Neos.Eel/Classes/ProtectedContext.php index d3cc0e87e0..bff5ede663 100644 --- a/Neos.Eel/Classes/ProtectedContext.php +++ b/Neos.Eel/Classes/ProtectedContext.php @@ -24,7 +24,7 @@ class ProtectedContext extends Context { /** - * @var array + * @var array */ protected $allowedMethods = []; @@ -32,7 +32,7 @@ class ProtectedContext extends Context * Call a method if it is allowed * * @param string $method - * @param array $arguments + * @param array $arguments * @return mixed|void * @throws NotAllowedException */ @@ -49,7 +49,7 @@ public function call($method, array $arguments = []) * * The list of allowed methods for the given path is applied to the new context. * - * @param string $path + * @param ProtectedContext|string|null $path * @return Context The wrapped value */ public function getAndWrap($path = null) @@ -82,7 +82,7 @@ public function getAndWrap($path = null) * * $context->allow(array('String.*', 'Array.reverse')); * - * @param array|string $pathOrMethods + * @param array|string $pathOrMethods * @return void */ public function allow($pathOrMethods) diff --git a/Neos.Eel/Classes/Utility.php b/Neos.Eel/Classes/Utility.php index 1b26575abd..ab7e0b3fe9 100644 --- a/Neos.Eel/Classes/Utility.php +++ b/Neos.Eel/Classes/Utility.php @@ -35,8 +35,8 @@ public static function parseEelExpression(string $expression): ?string * Get variables from configuration that should be set in the context by default. * For example Eel helpers are made available by this. * - * @param array $configuration An one dimensional associative array of context variable paths mapping to object names - * @return array Array with default context variable objects. + * @param array $configuration An one dimensional associative array of context variable paths mapping to object names + * @return array Array with default context variable objects. */ public static function getDefaultContextVariables(array $configuration) { @@ -45,6 +45,7 @@ public static function getDefaultContextVariables(array $configuration) $currentPathBase = & $defaultContextVariables; $variablePathNames = explode('.', $variableName); foreach ($variablePathNames as $pathName) { + /** @phpstan-ignore isset.offset (not so sure here) */ if (!isset($currentPathBase[$pathName])) { $currentPathBase[$pathName] = []; } @@ -66,15 +67,19 @@ public static function getDefaultContextVariables(array $configuration) /** * Create a closure to be used as Helper for eel. * - * @param string $objectConfiguration className followed by two colone and the method name + * @param string $objectConfiguration className followed by two colons and the method name * @return callable */ private static function createClosureFromConfiguration(string $objectConfiguration): callable { list($className, $methodName) = explode('::', $objectConfiguration, 2); - return function (...$arguments) use ($className, $methodName) { + return function (...$arguments) use ($className, $methodName, $objectConfiguration) { + $callback = [$className, $methodName]; + if (!is_callable($callback)) { + throw new \InvalidArgumentException($objectConfiguration . ' is not callable', 1744529247); + } return call_user_func_array( - [$className, $methodName], + $callback, $arguments ); }; @@ -85,8 +90,8 @@ private static function createClosureFromConfiguration(string $objectConfigurati * * @param string $expression * @param EelEvaluatorInterface $eelEvaluator - * @param array $contextVariables - * @param array $defaultContextConfiguration + * @param array $contextVariables + * @param array $defaultContextConfiguration * @return mixed * @throws Exception */ diff --git a/Neos.Error.Messages/Classes/Message.php b/Neos.Error.Messages/Classes/Message.php index 67f6464d0c..5f57b6baf3 100644 --- a/Neos.Error.Messages/Classes/Message.php +++ b/Neos.Error.Messages/Classes/Message.php @@ -43,7 +43,7 @@ class Message /** * The message arguments. Will be replaced in the message body. - * @var array + * @var array */ protected $arguments = []; @@ -58,7 +58,7 @@ class Message * * @param string $message An english error message which is used if no other error message can be resolved * @param integer|null $code A unique error code - * @param array $arguments Array of arguments to be replaced in message + * @param array $arguments Array of arguments to be replaced in message * @param string $title optional title for the message * @api */ @@ -102,7 +102,7 @@ public function getCode(): int } /** - * @return array + * @return array * @api */ public function getArguments(): array diff --git a/Neos.Error.Messages/Classes/Result.php b/Neos.Error.Messages/Classes/Result.php index f363a092a8..5a82b0635e 100644 --- a/Neos.Error.Messages/Classes/Result.php +++ b/Neos.Error.Messages/Classes/Result.php @@ -128,79 +128,86 @@ public function addNotice(Notice $notice) /** * Get all errors in the current Result object (non-recursive) * - * @param string $messageTypeFilter if specified only errors implementing the given class are returned + * @template T of Error + * @param class-string|null $messageTypeFilter if specified only errors implementing the given class are returned * @return array * @api */ public function getErrors(?string $messageTypeFilter = null): array { - return $this->filterMessages($this->errors, $messageTypeFilter); + return $this->filterMessages($this->errors, $messageTypeFilter ?: Error::class); } /** * Get all warnings in the current Result object (non-recursive) * - * @param string $messageTypeFilter if specified only warnings implementing the given class are returned + * @template T of Warning + * @param class-string|null $messageTypeFilter if specified only warnings implementing the given class are returned * @return array * @api */ public function getWarnings(?string $messageTypeFilter = null): array { - return $this->filterMessages($this->warnings, $messageTypeFilter); + return $this->filterMessages($this->warnings, $messageTypeFilter ?: Warning::class); } /** * Get all notices in the current Result object (non-recursive) * - * @param string $messageTypeFilter if specified only notices implementing the given class are returned + * @template T of Notice + * @param class-string|null $messageTypeFilter if specified only notices implementing the given class are returned * @return array * @api */ public function getNotices(?string $messageTypeFilter = null): array { - return $this->filterMessages($this->notices, $messageTypeFilter); + return $this->filterMessages($this->notices, $messageTypeFilter ?: Notice::class); } /** * Get the first error object of the current Result object (non-recursive) * - * @param string $messageTypeFilter if specified only errors implementing the given class are considered - * @return Error + * @template T of Error + * @param class-string|null $messageTypeFilter if specified only errors implementing the given class are considered + * @return ($messageTypeFilter is null ? ?Error : ?T) * @api */ public function getFirstError(?string $messageTypeFilter = null) { $matchingErrors = $this->filterMessages($this->errors, $messageTypeFilter); - reset($matchingErrors); - return current($matchingErrors); + $result = reset($matchingErrors); + + return $result instanceof Error ? $result : null; } /** * Get the first warning object of the current Result object (non-recursive) * - * @param string $messageTypeFilter if specified only warnings implementing the given class are considered - * @return Warning + * @template T of Warning + * @param ?class-string $messageTypeFilter if specified only warnings implementing the given class are considered + * @return ($messageTypeFilter is null ? ?Warning : ?T) * @api */ public function getFirstWarning(?string $messageTypeFilter = null) { - $matchingWarnings = $this->filterMessages($this->warnings, $messageTypeFilter); + $matchingWarnings = $this->filterMessages($this->warnings, $messageTypeFilter ?: Warning::class); reset($matchingWarnings); - return current($matchingWarnings); + return current($matchingWarnings) ?: null; } /** * Get the first notice object of the current Result object (non-recursive) * - * @param string $messageTypeFilter if specified only notices implementing the given class are considered - * @return Notice + * @template T of Notice + * @param ?class-string $messageTypeFilter if specified only notices implementing the given class are considered + * @return ($messageTypeFilter is null ? ?Notice : ?T) * @api */ public function getFirstNotice(?string $messageTypeFilter = null) { - $matchingNotices = $this->filterMessages($this->notices, $messageTypeFilter); + $matchingNotices = $this->filterMessages($this->notices, $messageTypeFilter ?: Notice::class); reset($matchingNotices); - return current($matchingNotices); + return current($matchingNotices) ?: null; } /** @@ -232,7 +239,7 @@ public function forProperty(?string $propertyPath = null): Result /** * Internal use only! * - * @param array $pathSegments + * @param array $pathSegments * @return Result */ public function recurseThroughResult(array $pathSegments): Result @@ -249,7 +256,6 @@ public function recurseThroughResult(array $pathSegments): Result $this->propertyResults[$propertyName] = $newResult; } - /** @var Result $result */ $result = $this->propertyResults[$propertyName]; return $result->recurseThroughResult($pathSegments); } @@ -360,8 +366,9 @@ public function getFlattenedErrors(): array * where the key is the property path where the error occurred, and the * value is a list of all errors (stored as array) * - * @param string $type - * @return array> + * @template T of Error + * @param class-string $type + * @return array> * @api */ public function getFlattenedErrorsOfType(string $type): array @@ -404,11 +411,11 @@ public function getFlattenedNotices(): array /** * Flatten a tree of Result objects, based on a certain property. * + * @template T of Message * @param string $propertyName - * @param array $result The current result to be flattened - * @param array $level The property path in the format array('level1', 'level2', ...) for recursion - * @param string $messageTypeFilter If specified only messages implementing the given class name are taken into account - * @param-out array> $result + * @param array $result The current result to be flattened + * @param array $level The property path in the format array('level1', 'level2', ...) for recursion + * @param ?class-string $messageTypeFilter $messageTypeFilter If specified only messages implementing the given class name are taken into account * @return void */ public function flattenTree(string $propertyName, array &$result, array $level = [], ?string $messageTypeFilter = null) @@ -417,7 +424,6 @@ public function flattenTree(string $propertyName, array &$result, array $level = $propertyPath = implode('.', $level); $result[$propertyPath] = $this->filterMessages($this->$propertyName, $messageTypeFilter); } - /** @var Result $subResult */ foreach ($this->propertyResults as $subPropertyName => $subResult) { array_push($level, $subPropertyName); $subResult->flattenTree($propertyName, $result, $level, $messageTypeFilter); @@ -426,9 +432,10 @@ public function flattenTree(string $propertyName, array &$result, array $level = } /** - * @param Message[] $messages an array of Message instances to filter - * @param string $messageTypeFilter If specified only messages implementing the given class name are taken into account - * @return array the filtered message instances + * @template T of Message + * @param array $messages an array of Message instances to filter + * @param ?class-string $messageTypeFilter If specified only messages implementing the given class name are taken into account + * @return ($messageTypeFilter is null ? array : array) array the filtered message instances */ protected function filterMessages(array $messages, ?string $messageTypeFilter = null): array { diff --git a/Neos.Flow.Log/Classes/Backend/AbstractBackend.php b/Neos.Flow.Log/Classes/Backend/AbstractBackend.php index 6279d6cd85..1e879d90db 100644 --- a/Neos.Flow.Log/Classes/Backend/AbstractBackend.php +++ b/Neos.Flow.Log/Classes/Backend/AbstractBackend.php @@ -42,7 +42,7 @@ abstract class AbstractBackend implements BackendInterface */ public function __construct($options = []) { - if (is_array($options) || $options instanceof \ArrayAccess) { + if (is_iterable($options)) { foreach ($options as $optionKey => $optionValue) { $methodName = 'set' . ucfirst($optionKey); if (method_exists($this, $methodName)) { diff --git a/Neos.Flow.Log/Classes/Backend/AnsiConsoleBackend.php b/Neos.Flow.Log/Classes/Backend/AnsiConsoleBackend.php index 5749b5df81..30784f9aef 100644 --- a/Neos.Flow.Log/Classes/Backend/AnsiConsoleBackend.php +++ b/Neos.Flow.Log/Classes/Backend/AnsiConsoleBackend.php @@ -40,7 +40,7 @@ class AnsiConsoleBackend extends ConsoleBackend const END = "\033[0m"; /** - * @var array + * @var array */ protected $tagFormats = []; @@ -75,12 +75,7 @@ public function open(): void /** * Appends the given message along with the additional information into the log. * - * @param string $message - * @param int $severity - * @param array $additionalData - * @param string $packageKey - * @param string $className - * @param string $methodName + * @param array|null $additionalData * @return void */ public function append(string $message, int $severity = LOG_INFO, $additionalData = null, ?string $packageKey = null, ?string $className = null, ?string $methodName = null): void @@ -118,7 +113,7 @@ protected function formatOutput($output) } else { return str_replace('|', $matches[3], $format); } - }, $output); + }, $output) ?: ''; } while ($lastOutput !== $output); return $output; } @@ -126,7 +121,7 @@ protected function formatOutput($output) /** * @param boolean $disableAnsi */ - public function setDisableAnsi($disableAnsi) + public function setDisableAnsi($disableAnsi): void { $this->disableAnsi = $disableAnsi; } diff --git a/Neos.Flow.Log/Classes/Backend/ConsoleBackend.php b/Neos.Flow.Log/Classes/Backend/ConsoleBackend.php index def5dd734e..1398b288c9 100644 --- a/Neos.Flow.Log/Classes/Backend/ConsoleBackend.php +++ b/Neos.Flow.Log/Classes/Backend/ConsoleBackend.php @@ -25,7 +25,7 @@ class ConsoleBackend extends AbstractBackend { /** * An array of severity labels, indexed by their integer constant - * @var array + * @var array */ protected $severityLabels; @@ -61,10 +61,11 @@ public function open(): void LOG_DEBUG => 'DEBUG ', ]; - $this->streamHandle = fopen('php://' . $this->streamName, 'w'); - if (!is_resource($this->streamHandle)) { + $streamHandle = fopen('php://' . $this->streamName, 'w'); + if (!is_resource($streamHandle)) { throw new CouldNotOpenResourceException('Could not open stream "' . $this->streamName . '" for write access.', 1310986609); } + $this->streamHandle = $streamHandle; } /** diff --git a/Neos.Flow.Log/Classes/Backend/FileBackend.php b/Neos.Flow.Log/Classes/Backend/FileBackend.php index 67a3ac2aaf..7a19df4a97 100644 --- a/Neos.Flow.Log/Classes/Backend/FileBackend.php +++ b/Neos.Flow.Log/Classes/Backend/FileBackend.php @@ -27,7 +27,7 @@ class FileBackend extends AbstractBackend { /** * An array of severity labels, indexed by their integer constant - * @var array + * @var array */ protected $severityLabels; diff --git a/Neos.Flow.Log/Classes/PlainTextFormatter.php b/Neos.Flow.Log/Classes/PlainTextFormatter.php index 0a15efb126..5590dbbed2 100644 --- a/Neos.Flow.Log/Classes/PlainTextFormatter.php +++ b/Neos.Flow.Log/Classes/PlainTextFormatter.php @@ -22,10 +22,9 @@ public function __construct($variable) } /** - * @param $spaces - * @return string + * @return ?string */ - public function format($spaces = 4) + public function format(int $spaces = 4) { return $this->renderVariableAsPlaintext($this->variable, $spaces); } diff --git a/Neos.Flow.Log/Classes/Psr/Logger.php b/Neos.Flow.Log/Classes/Psr/Logger.php index 6ebad73f56..c1a18be6f8 100644 --- a/Neos.Flow.Log/Classes/Psr/Logger.php +++ b/Neos.Flow.Log/Classes/Psr/Logger.php @@ -45,7 +45,7 @@ class Logger implements LoggerInterface /** * Constructs the PSR-3 Logger. * - * @param iterable $backends + * @param iterable $backends */ public function __construct(iterable $backends) { @@ -61,7 +61,7 @@ public function __construct(iterable $backends) /** * @param mixed $level * @param string|\Stringable $message - * @param array $context + * @param array $context * @api */ public function log($level, string|\Stringable $message, array $context = []): void @@ -80,8 +80,8 @@ public function log($level, string|\Stringable $message, array $context = []): v } /** - * @param array $context - * @return array list of packageKey, className and methodName either string or null + * @param array $context + * @return array list of packageKey, className and methodName either string or null */ protected function extractLegacyDataFromContext(array $context): array { @@ -93,8 +93,8 @@ protected function extractLegacyDataFromContext(array $context): array } /** - * @param array $context - * @return array + * @param array $context + * @return array */ protected function removeLegacyDataFromContext(array $context): array { diff --git a/Neos.Flow/Classes/Annotations/Introduce.php b/Neos.Flow/Classes/Annotations/Introduce.php index 2378ffecf0..2a514cf9c3 100644 --- a/Neos.Flow/Classes/Annotations/Introduce.php +++ b/Neos.Flow/Classes/Annotations/Introduce.php @@ -33,17 +33,21 @@ final class Introduce /** * The interface name to introduce. - * @var string|null + * @var class-string|null */ public $interfaceName; /** * The trait name to introduce * - * @var string|null + * @var trait-string|null */ public $traitName; + /** + * @param class-string|null $interfaceName + * @param trait-string|null $traitName + */ public function __construct(string $pointcutExpression, ?string $interfaceName = null, ?string $traitName = null) { $this->pointcutExpression = $pointcutExpression; diff --git a/Neos.Flow/Classes/Annotations/Route.php b/Neos.Flow/Classes/Annotations/Route.php index 4d9e8d86d3..cf2ca2c2a7 100644 --- a/Neos.Flow/Classes/Annotations/Route.php +++ b/Neos.Flow/Classes/Annotations/Route.php @@ -38,8 +38,8 @@ /** * @param string $uriPattern The uri-pattern for the route without leading '/'. Might contain route values in the form of `path/{foo}` * @param string $name The suffix of the route name as shown in `route:list` (defaults to the action name: "My.Package :: Site :: index") - * @param array $httpMethods List of uppercase http verbs like 'GET', 'POST', 'PUT', 'DELETE', if not specified any request method will be matched - * @param array $defaults Values to set for this route + * @param list $httpMethods List of uppercase http verbs like 'GET', 'POST', 'PUT', 'DELETE', if not specified any request method will be matched + * @param array $defaults Values to set for this route */ public function __construct( public string $uriPattern, diff --git a/Neos.Flow/Classes/Annotations/Validate.php b/Neos.Flow/Classes/Annotations/Validate.php index eccf3927db..db627d01e0 100644 --- a/Neos.Flow/Classes/Annotations/Validate.php +++ b/Neos.Flow/Classes/Annotations/Validate.php @@ -31,7 +31,7 @@ final class Validate /** * Options for the validator, validator-specific. - * @var array + * @var array */ public $options = []; @@ -43,10 +43,14 @@ final class Validate /** * The validation groups for which this validator should be executed. - * @var array + * @var array */ public $validationGroups = ['Default']; + /** + * @param array $options + * @param array|null $validationGroups + */ public function __construct(?string $argumentName = null, ?string $type = null, array $options = [], ?array $validationGroups = null) { $this->type = $type; diff --git a/Neos.Flow/Classes/Annotations/ValidationGroups.php b/Neos.Flow/Classes/Annotations/ValidationGroups.php index fcac75c8b6..838efbc0d2 100644 --- a/Neos.Flow/Classes/Annotations/ValidationGroups.php +++ b/Neos.Flow/Classes/Annotations/ValidationGroups.php @@ -24,10 +24,13 @@ final class ValidationGroups { /** * The validation groups for which validation on this method should be executed. (Can be given as anonymous argument.) - * @var array + * @var array */ public $validationGroups = ['Default', 'Controller']; + /** + * @param array $validationGroups + */ public function __construct(array $validationGroups) { $this->validationGroups = $validationGroups; diff --git a/Neos.Flow/Classes/Aop/Advice/AbstractAdvice.php b/Neos.Flow/Classes/Aop/Advice/AbstractAdvice.php index e77c1eddc8..a1fa1640c8 100644 --- a/Neos.Flow/Classes/Aop/Advice/AbstractAdvice.php +++ b/Neos.Flow/Classes/Aop/Advice/AbstractAdvice.php @@ -24,7 +24,7 @@ class AbstractAdvice implements AdviceInterface { /** * Holds the name of the aspect object containing the advice - * @var string + * @var class-string */ protected $aspectObjectName; @@ -42,29 +42,29 @@ class AbstractAdvice implements AdviceInterface /** * A reference to the Object Manager - * @var ObjectManagerInterface + * @var ?ObjectManagerInterface */ protected $objectManager; /** * Runtime evaluations definition array - * @var array + * @var array */ protected $runtimeEvaluationsDefinition; /** * Runtime evaluations function - * @var \Closure + * @var ?\Closure */ protected $runtimeEvaluator; /** * Constructor * - * @param string $aspectObjectName Name of the aspect object containing the advice + * @param class-string $aspectObjectName Name of the aspect object containing the advice * @param string $adviceMethodName Name of the advice method - * @param ObjectManagerInterface $objectManager Only require if a runtime evaluations function is specified - * @param \Closure $runtimeEvaluator Runtime evaluations function + * @param ?ObjectManagerInterface $objectManager Only require if a runtime evaluations function is specified + * @param ?\Closure $runtimeEvaluator Runtime evaluations function */ public function __construct(string $aspectObjectName, string $adviceMethodName, ?ObjectManagerInterface $objectManager = null, ?\Closure $runtimeEvaluator = null) { @@ -86,6 +86,9 @@ public function invoke(JoinPointInterface $joinPoint) return; } + if ($this->objectManager === null) { + throw new \RuntimeException('Object manager is missing', 1744496555); + } $adviceObject = $this->objectManager->get($this->aspectObjectName); $methodName = $this->adviceMethodName; $adviceObject->$methodName($joinPoint); @@ -96,7 +99,7 @@ public function invoke(JoinPointInterface $joinPoint) /** * Returns the aspect's object name which has been passed to the constructor * - * @return string The object name of the aspect + * @return class-string The object name of the aspect */ public function getAspectObjectName(): string { @@ -127,6 +130,9 @@ public function getAdviceMethodName(): string protected function emitAdviceInvoked($aspectObject, string $methodName, JoinPointInterface $joinPoint): void { if ($this->dispatcher === null) { + if ($this->objectManager === null) { + throw new \RuntimeException('Object manager is missing', 1744496534); + } $this->dispatcher = $this->objectManager->get(Dispatcher::class); } diff --git a/Neos.Flow/Classes/Aop/Advice/AdviceChain.php b/Neos.Flow/Classes/Aop/Advice/AdviceChain.php index 21da910d4b..7349fb8eee 100644 --- a/Neos.Flow/Classes/Aop/Advice/AdviceChain.php +++ b/Neos.Flow/Classes/Aop/Advice/AdviceChain.php @@ -22,7 +22,7 @@ class AdviceChain { /** * An array of Advice objects which form the advice chain - * @var array + * @var array */ protected $advices; @@ -35,7 +35,7 @@ class AdviceChain /** * Initializes the advice chain * - * @param array $advices An array of AdviceInterface compatible objects which form the chain of advices + * @param array $advices An array of AdviceInterface compatible objects which form the chain of advices */ public function __construct(array $advices) { @@ -48,7 +48,7 @@ public function __construct(array $advices) * left in the chain, the proxy classes' method invokeJoinpoint() will finally * be called. * - * @param JoinPointInterface $joinPoint The current join point (ie. the context) + * @param JoinPointInterface $joinPoint The current join point (ie. the context) * @return mixed Result of the advice or the original method of the target class */ public function proceed(JoinPointInterface &$joinPoint) diff --git a/Neos.Flow/Classes/Aop/Advice/AroundAdvice.php b/Neos.Flow/Classes/Aop/Advice/AroundAdvice.php index 8f58cbceac..ed5ad56833 100644 --- a/Neos.Flow/Classes/Aop/Advice/AroundAdvice.php +++ b/Neos.Flow/Classes/Aop/Advice/AroundAdvice.php @@ -30,6 +30,9 @@ public function invoke(JoinPointInterface $joinPoint) return $joinPoint->getAdviceChain()->proceed($joinPoint); } + if ($this->objectManager === null) { + throw new \RuntimeException('Object manager is missing', 1744496510); + } $adviceObject = $this->objectManager->get($this->aspectObjectName); $methodName = $this->adviceMethodName; $result = $adviceObject->$methodName($joinPoint); diff --git a/Neos.Flow/Classes/Aop/AdvicesTrait.php b/Neos.Flow/Classes/Aop/AdvicesTrait.php index 9cd4793be9..e28cf39816 100644 --- a/Neos.Flow/Classes/Aop/AdvicesTrait.php +++ b/Neos.Flow/Classes/Aop/AdvicesTrait.php @@ -14,6 +14,7 @@ /** * Contains boilerplate code for AOP execution and is added to AOP proxy classes. * + * @phpstan-ignore trait.unused (probably API) */ trait AdvicesTrait { diff --git a/Neos.Flow/Classes/Aop/AspectContainer.php b/Neos.Flow/Classes/Aop/AspectContainer.php index c66d8a6b58..89cab12f0b 100644 --- a/Neos.Flow/Classes/Aop/AspectContainer.php +++ b/Neos.Flow/Classes/Aop/AspectContainer.php @@ -38,37 +38,32 @@ class AspectContainer { /** - * @var string + * @var class-string */ protected $className; /** - * An array of \Neos\Flow\Aop\Advisor objects - * @var array + * @var array */ protected $advisors = []; /** - * An array of \Neos\Flow\Aop\InterfaceIntroduction objects - * @var array + * @var array */ protected $interfaceIntroductions = []; /** - * An array of \Neos\Flow\Aop\PropertyIntroduction objects - * @var array + * @var array */ protected $propertyIntroductions = []; /** - * An array of \Neos\Flow\Aop\TraitIntroduction objects - * @var array + * @var array */ protected $traitIntroductions = []; /** - * An array of explicitly declared \Neos\Flow\Pointcut objects - * @var array + * @var array */ protected $pointcuts = []; @@ -80,7 +75,7 @@ class AspectContainer /** * The constructor * - * @param string $className Name of the aspect class + * @param class-string $className Name of the aspect class */ public function __construct(string $className) { @@ -90,7 +85,7 @@ public function __construct(string $className) /** * Returns the name of the aspect class * - * @return string Name of the aspect class + * @return class-string Name of the aspect class */ public function getClassName(): string { @@ -100,7 +95,7 @@ public function getClassName(): string /** * Returns the advisors which were defined in the aspect * - * @return array Array of \Neos\Flow\Aop\Advisor objects + * @return array */ public function getAdvisors(): array { @@ -110,7 +105,7 @@ public function getAdvisors(): array /** * Returns the interface introductions which were defined in the aspect * - * @return array Array of \Neos\Flow\Aop\InterfaceIntroduction objects + * @return array */ public function getInterfaceIntroductions(): array { @@ -120,7 +115,7 @@ public function getInterfaceIntroductions(): array /** * Returns the property introductions which were defined in the aspect * - * @return array Array of \Neos\Flow\Aop\PropertyIntroduction objects + * @return array */ public function getPropertyIntroductions(): array { @@ -130,7 +125,7 @@ public function getPropertyIntroductions(): array /** * Returns the trait introductions which were defined in the aspect * - * @return array Array of \Neos\Flow\Aop\TraitIntroduction objects + * @return array Array of \Neos\Flow\Aop\TraitIntroduction objects */ public function getTraitIntroductions(): array { @@ -142,7 +137,7 @@ public function getTraitIntroductions(): array * does not contain the pointcuts which were made out of the pointcut * expressions for the advisors! * - * @return array Array of \Neos\Flow\Aop\Pointcut\Pointcut objects + * @return array */ public function getPointcuts(): array { diff --git a/Neos.Flow/Classes/Aop/Builder/AbstractMethodInterceptorBuilder.php b/Neos.Flow/Classes/Aop/Builder/AbstractMethodInterceptorBuilder.php index b3b4d987e3..59d02dcd04 100644 --- a/Neos.Flow/Classes/Aop/Builder/AbstractMethodInterceptorBuilder.php +++ b/Neos.Flow/Classes/Aop/Builder/AbstractMethodInterceptorBuilder.php @@ -54,8 +54,8 @@ public function injectCompiler(Compiler $compiler): void * Builds method interception PHP code * * @param string $methodName Name of the method to build an interceptor for - * @param array $methodMetaInformation An array of method names and their meta information, including advices for the method (if any) - * @param string $targetClassName Name of the target class to build the interceptor for + * @param array $methodMetaInformation An array of method names and their meta information, including advices for the method (if any) + * @param class-string $targetClassName Name of the target class to build the interceptor for * @return void */ abstract public function build(string $methodName, array $methodMetaInformation, string $targetClassName): void; @@ -63,7 +63,7 @@ abstract public function build(string $methodName, array $methodMetaInformation, /** * Builds a string containing PHP code to build the array given as input. * - * @param array $array + * @param array $array * @return string e.g. 'array()' or 'array(1 => 'bar') */ protected function buildArraySetupCode(array $array): string @@ -91,7 +91,7 @@ protected function buildArraySetupCode(array $array): string * the constructor of a new join point. Used in the method interceptor * functions. * - * @param string|null $className Name of the declaring class of the method + * @param class-string|null $className Name of the declaring class of the method * @param string|null $methodName Name of the method to create arguments array code for * @param bool $useArgumentsArray If set, the $methodArguments array will be built from $arguments instead of using the actual parameter variables. * @return string The generated code to be used in an "array()" definition @@ -126,8 +126,9 @@ protected function buildMethodArgumentsArrayCode(?string $className = null, ?str /** * Generates the parameters code needed to call the constructor with the saved parameters. * - * @param string|null $className Name of the class the method is declared in + * @param class-string|null $className Name of the class the method is declared in * @return string The generated parameters code + * @todo can this be removed? */ protected function buildSavedConstructorParametersCode(?string $className = null): string { @@ -150,10 +151,10 @@ protected function buildSavedConstructorParametersCode(?string $className = null /** * Builds the advice interception code, to be used in a method interceptor. * - * @param array $groupedAdvices The advices grouped by advice type + * @param array $groupedAdvices The advices grouped by advice type * @param string|null $methodName Name of the method the advice applies to - * @param string|null $targetClassName Name of the target class - * @param string|null $declaringClassName Name of the declaring class. This is usually the same as the $targetClassName. However, it is the introduction interface for introduced methods. + * @param class-string|null $targetClassName Name of the target class + * @param class-string|null $declaringClassName Name of the declaring class. This is usually the same as the $targetClassName. However, it is the introduction interface for introduced methods. * @return string PHP code to be used in the method interceptor */ protected function buildAdvicesCode(array $groupedAdvices, ?string $methodName = null, ?string $targetClassName = null, ?string $declaringClassName = null): string diff --git a/Neos.Flow/Classes/Aop/Builder/AdvisedConstructorInterceptorBuilder.php b/Neos.Flow/Classes/Aop/Builder/AdvisedConstructorInterceptorBuilder.php index baaa0c188e..219e9cc4ad 100644 --- a/Neos.Flow/Classes/Aop/Builder/AdvisedConstructorInterceptorBuilder.php +++ b/Neos.Flow/Classes/Aop/Builder/AdvisedConstructorInterceptorBuilder.php @@ -26,8 +26,8 @@ class AdvisedConstructorInterceptorBuilder extends AbstractMethodInterceptorBuil * Builds interception PHP code for an advised constructor * * @param string $methodName Name of the method to build an interceptor for - * @param array $methodMetaInformation An array of method names and their meta information, including advices for the method (if any) - * @param string $targetClassName Name of the target class to build the interceptor for + * @param array $methodMetaInformation An array of method names and their meta information, including advices for the method (if any) + * @param class-string $targetClassName Name of the target class to build the interceptor for * @return void * @throws Exception */ @@ -38,7 +38,11 @@ public function build(string $methodName, array $methodMetaInformation, string $ } $declaringClassName = $methodMetaInformation[$methodName]['declaringClassName']; - $proxyMethod = $this->compiler->getProxyClass($targetClassName)->getConstructor(); + $proxyClass = $this->compiler->getProxyClass($targetClassName); + if ($proxyClass === false) { + throw new \InvalidArgumentException('Cannot build proxy class for '. $targetClassName, 1744495809); + } + $proxyMethod = $proxyClass->getConstructor(); $groupedAdvices = $methodMetaInformation[$methodName]['groupedAdvices']; $advicesCode = $this->buildAdvicesCode($groupedAdvices, $methodName, $targetClassName, $declaringClassName); diff --git a/Neos.Flow/Classes/Aop/Builder/AdvisedMethodInterceptorBuilder.php b/Neos.Flow/Classes/Aop/Builder/AdvisedMethodInterceptorBuilder.php index e0f7107500..b2e49160a7 100644 --- a/Neos.Flow/Classes/Aop/Builder/AdvisedMethodInterceptorBuilder.php +++ b/Neos.Flow/Classes/Aop/Builder/AdvisedMethodInterceptorBuilder.php @@ -27,8 +27,8 @@ class AdvisedMethodInterceptorBuilder extends AbstractMethodInterceptorBuilder * Builds interception PHP code for an advised method * * @param string $methodName Name of the method to build an interceptor for - * @param array $methodMetaInformation An array of method names and their meta information, including advices for the method (if any) - * @param string $targetClassName Name of the target class to build the interceptor for + * @param array $methodMetaInformation An array of method names and their meta information, including advices for the method (if any) + * @param class-string $targetClassName Name of the target class to build the interceptor for * @return void * @throws Exception */ @@ -39,7 +39,11 @@ public function build(string $methodName, array $methodMetaInformation, string $ } $declaringClassName = $methodMetaInformation[$methodName]['declaringClassName']; - $proxyMethod = $this->compiler->getProxyClass($targetClassName)->getMethod($methodName); + $proxyClass = $this->compiler->getProxyClass($targetClassName); + if ($proxyClass === false) { + throw new \InvalidArgumentException('Cannot build proxy class for ' . $targetClassName); + } + $proxyMethod = $proxyClass->getMethod($methodName); if ($proxyMethod->getVisibility() === ProxyMethodGenerator::VISIBILITY_PRIVATE) { throw new Exception(sprintf('The %s cannot build interceptor code for private method %s::%s(). Please change the scope to at least protected or adjust the pointcut expression in the corresponding aspect.', __CLASS__, $targetClassName, $methodName), 1593070574); } diff --git a/Neos.Flow/Classes/Aop/Builder/ClassNameIndex.php b/Neos.Flow/Classes/Aop/Builder/ClassNameIndex.php index a8a4a7046a..e7c71924f2 100644 --- a/Neos.Flow/Classes/Aop/Builder/ClassNameIndex.php +++ b/Neos.Flow/Classes/Aop/Builder/ClassNameIndex.php @@ -23,7 +23,7 @@ class ClassNameIndex { /** * Indexed array by class name - * @var array + * @var array */ protected $classNames = []; @@ -31,7 +31,7 @@ class ClassNameIndex * Constructor. Note: If you pass a data array here, make sure * to key sort it before! * - * @param array $classNames Array with class names as keys + * @param array $classNames Array with class names as keys */ public function __construct(array $classNames = []) { @@ -42,7 +42,7 @@ public function __construct(array $classNames = []) * Set the data of this index to the given class * names. Note: Make sure to sort the array before! * - * @param array $classNames + * @param array $classNames * @return void */ public function setClassNames(array $classNames): void @@ -53,7 +53,7 @@ public function setClassNames(array $classNames): void /** * Returns the class names contained in this index * - * @return array An array of class names contained in this index + * @return array An array of class names contained in this index */ public function getClassNames(): array { @@ -63,7 +63,7 @@ public function getClassNames(): array /** * Checks, if a class name is contained in this index * - * @param string $className The class name to check for + * @param class-string $className The class name to check for * @return bool true, if the given class name is contained in this index */ public function hasClassName(string $className): bool diff --git a/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php b/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php index a175e0cf8d..72dc52499f 100644 --- a/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php +++ b/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php @@ -49,18 +49,29 @@ class ProxyClassBuilder protected LoggerInterface $logger; protected PointcutExpressionParser $pointcutExpressionParser; protected VariableFrontend $objectConfigurationCache; + /** @var CompileTimeObjectManager */ protected CompileTimeObjectManager $objectManager; /** * Hardcoded list of Flow sub packages (first 15 characters) which must be immune to AOP proxying for security, technical or conceptual reasons. + * + * @var array */ protected array $excludedSubPackages = ['Neos\Flow\Aop\\', 'Neos\Flow\Cach', 'Neos\Flow\Erro', 'Neos\Flow\Log\\', 'Neos\Flow\Moni', 'Neos\Flow\Obje', 'Neos\Flow\Pack', 'Neos\Flow\Prop', 'Neos\Flow\Refl', 'Neos\Flow\Util', 'Neos\Flow\Vali']; /** * A registry of all known aspects + * + * @var array */ protected array $aspectContainers = []; + /** + * @var array{ + * AdvisedConstructor?: AdvisedConstructorInterceptorBuilder, + * AdvisedMethod?: AdvisedMethodInterceptorBuilder, + * } + */ protected array $methodInterceptorBuilders = []; public function injectCompiler(Compiler $compiler): void @@ -100,6 +111,9 @@ public function injectAdvisedMethodInterceptorBuilder(AdvisedMethodInterceptorBu $this->methodInterceptorBuilders['AdvisedMethod'] = $builder; } + /** + * @param CompileTimeObjectManager $objectManager + */ public function injectObjectManager(CompileTimeObjectManager $objectManager): void { $this->objectManager = $objectManager; @@ -207,8 +221,8 @@ public function findPointcut(string $aspectClassName, string $pointcutMethodName * Determines which of the given classes are potentially proxyable * and returns their names in an array. * - * @param array $classNamesByPackage Names of the classes to check - * @return array Names of classes which can be proxied + * @param array> $classNamesByPackage Names of the classes to check + * @return array Names of classes which can be proxied */ protected function getProxyableClasses(array $classNamesByPackage): array { @@ -232,8 +246,9 @@ protected function getProxyableClasses(array $classNamesByPackage): array * Checks the annotations of the specified classes for aspect tags * and creates an aspect with advisors accordingly. * - * @param array $classNames Classes to check for aspect tags. - * @return array An array of Aop\AspectContainer for all aspects which were found. + * @template T of object + * @param array> $classNames Classes to check for aspect tags. + * @return array,AspectContainer> An array of Aop\AspectContainer for all aspects which were found. * @throws Exception * @throws FilesException * @throws InvalidPointcutExpressionException @@ -254,7 +269,7 @@ protected function buildAspectContainers(array $classNames): array * is tagged as an aspect. The object acting as an advice will already be * fetched (and therefore instantiated if necessary). * - * @param string $aspectClassName Name of the class which forms the aspect, contains advices etc. + * @param class-string $aspectClassName Name of the class which forms the aspect, contains advices etc. * @return AspectContainer The aspect container containing one or more advisors * @throws Exception * @throws InvalidPointcutExpressionException @@ -275,6 +290,7 @@ protected function buildAspectContainer(string $aspectClassName): AspectContaine $annotationClass = get_class($annotation); switch ($annotationClass) { case Flow\Around::class: + /** @var Flow\Around $annotation */ $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass)); $advice = new Aop\Advice\AroundAdvice($aspectClassName, $methodName); $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); @@ -282,6 +298,7 @@ protected function buildAspectContainer(string $aspectClassName): AspectContaine $aspectContainer->addAdvisor($advisor); break; case Flow\Before::class: + /** @var Flow\Before $annotation */ $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass)); $advice = new Aop\Advice\BeforeAdvice($aspectClassName, $methodName); $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); @@ -289,6 +306,7 @@ protected function buildAspectContainer(string $aspectClassName): AspectContaine $aspectContainer->addAdvisor($advisor); break; case Flow\AfterReturning::class: + /** @var Flow\AfterReturning $annotation */ $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass)); $advice = new Aop\Advice\AfterReturningAdvice($aspectClassName, $methodName); $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); @@ -296,6 +314,7 @@ protected function buildAspectContainer(string $aspectClassName): AspectContaine $aspectContainer->addAdvisor($advisor); break; case Flow\AfterThrowing::class: + /** @var Flow\AfterThrowing $annotation */ $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass)); $advice = new Aop\Advice\AfterThrowingAdvice($aspectClassName, $methodName); $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); @@ -303,6 +322,7 @@ protected function buildAspectContainer(string $aspectClassName): AspectContaine $aspectContainer->addAdvisor($advisor); break; case Flow\After::class: + /** @var Flow\After $annotation */ $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass)); $advice = new Aop\Advice\AfterAdvice($aspectClassName, $methodName); $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); @@ -310,6 +330,7 @@ protected function buildAspectContainer(string $aspectClassName): AspectContaine $aspectContainer->addAdvisor($advisor); break; case Flow\Pointcut::class: + /** @var Flow\Pointcut $annotation */ $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->expression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass)); $pointcut = new Pointcut($annotation->expression, $pointcutFilterComposite, $aspectClassName, $methodName); $aspectContainer->addPointcut($pointcut); @@ -358,8 +379,8 @@ protected function buildAspectContainer(string $aspectClassName): AspectContaine /** * Builds methods for a single AOP proxy class for the specified class. * - * @param string $targetClassName Name of the class to create a proxy class file for - * @param array $aspectContainers The array of aspect containers from the AOP Framework + * @param class-string $targetClassName Name of the class to create a proxy class file for + * @param array $aspectContainers The array of aspect containers from the AOP Framework * @return bool true if the proxy class could be built, false otherwise. * @throws \ReflectionException * @throws CannotBuildObjectException @@ -379,7 +400,15 @@ public function buildProxyClass(string $targetClassName, array $aspectContainers $methodsFromIntroducedInterfaces = $this->getIntroducedMethodsFromInterfaceIntroductions($interfaceIntroductions); $interceptedMethods = []; - $this->addAdvisedMethodsToInterceptedMethods($interceptedMethods, array_merge($methodsFromTargetClass, $methodsFromIntroducedInterfaces), $targetClassName, $aspectContainers); + $this->addAdvisedMethodsToInterceptedMethods( + $interceptedMethods, + array_merge( + $methodsFromTargetClass, + $methodsFromIntroducedInterfaces + ), + $targetClassName, + $aspectContainers + ); $this->addIntroducedMethodsToInterceptedMethods($interceptedMethods, $methodsFromIntroducedInterfaces); if (count($interceptedMethods) < 1 && count($introducedInterfaces) < 1 && count($introducedTraits) < 1 && count($propertyIntroductions) < 1) { @@ -471,7 +500,7 @@ protected function proxySubClassesOfClassToEnsureAdvices(string $className, Clas /** * Adds code to build the methods and advices array in case the parent class has some. * - * @param string $className + * @param class-string $className * @param ClassNameIndex $treatedSubClasses * @return ClassNameIndex * @throws \ReflectionException @@ -483,7 +512,7 @@ protected function addBuildMethodsAndAdvicesCodeToClass(string $className, Class return $treatedSubClasses; } - $treatedSubClasses = $treatedSubClasses->union(new ClassNameIndex([$className])); + $treatedSubClasses = $treatedSubClasses->union(new ClassNameIndex([$className => true])); // @fixme in 8.3+ if ($this->reflectionService->isClassReflected($className) === false) { return $treatedSubClasses; } @@ -503,8 +532,9 @@ protected function addBuildMethodsAndAdvicesCodeToClass(string $className, Class /** * Returns the methods of the target class. * - * @param string $targetClassName Name of the target class - * @return array Method information with declaring class and method name pairs + * @template T of object + * @param class-string $targetClassName Name of the target class + * @return array, 1: string}> Method information with declaring class and method name pairs * @throws \ReflectionException */ protected function getMethodsFromTargetClass(string $targetClassName): array @@ -539,7 +569,7 @@ protected function getMethodsFromTargetClass(string $targetClassName): array * ); * * - * @param array $methodsAndGroupedAdvices An array of method names and grouped advice objects + * @param array $methodsAndGroupedAdvices An array of method names and grouped advice objects * @return string PHP code for the content of an array of target method names and advice objects * @see buildProxyClass() */ @@ -574,8 +604,8 @@ protected function buildMethodsAndAdvicesArrayCode(array $methodsAndGroupedAdvic * The generated code is added directly to the proxy class by calling the respective * methods of the Compiler API. * - * @param string $targetClassName The target class the pointcut should match with - * @param array $interceptedMethods An array of method names which need to be intercepted + * @param class-string $targetClassName The target class the pointcut should match with + * @param array}> $interceptedMethods An array of method names which need to be intercepted * @return void * @throws Aop\Exception\VoidImplementationException */ @@ -586,7 +616,11 @@ protected function buildMethodsInterceptorCode(string $targetClassName, array $i throw new Aop\Exception\VoidImplementationException(sprintf('Refuse to introduce method %s into target class %s because it has no implementation code. You might want to create an around advice which implements this method.', $methodName, $targetClassName), 1303224472); } $builderType = 'Advised' . ($methodName === '__construct' ? 'Constructor' : 'Method'); - $this->methodInterceptorBuilders[$builderType]->build($methodName, $interceptedMethods, $targetClassName); + $builder = $this->methodInterceptorBuilders[$builderType] ?? null; + if ($builder === null) { + throw new \RuntimeException('MethodInterceptorBuilder for ' . $builderType . ' not set', 1744496282); + } + $builder->build($methodName, $interceptedMethods, $targetClassName); } } @@ -594,10 +628,10 @@ protected function buildMethodsInterceptorCode(string $targetClassName, array $i * Traverses all aspect containers, their aspects and their advisors and adds the * methods and their advices to the (usually empty) array of intercepted methods. * - * @param array &$interceptedMethods An array (empty or not) which contains the names of the intercepted methods and additional information - * @param array $methods An array of class and method names which are matched against the pointcut (class name = name of the class or interface the method was declared) - * @param string $targetClassName Name of the class the pointcut should match with - * @param array &$aspectContainers All aspects to take into consideration + * @param array &$interceptedMethods An array (empty or not) which contains the names of the intercepted methods and additional information + * @param array $methods An array of class and method names which are matched against the pointcut (class name = name of the class or interface the method was declared) + * @param class-string $targetClassName Name of the class the pointcut should match with + * @param array $aspectContainers All aspects to take into consideration * @return void */ protected function addAdvisedMethodsToInterceptedMethods(array &$interceptedMethods, array $methods, string $targetClassName, array $aspectContainers): void @@ -635,8 +669,8 @@ protected function addAdvisedMethodsToInterceptedMethods(array &$interceptedMeth * Traverses all methods which were introduced by interfaces and adds them to the * intercepted methods array if they didn't exist already. * - * @param array &$interceptedMethods An array (empty or not) which contains the names of the intercepted methods and additional information - * @param array $methodsFromIntroducedInterfaces An array of class and method names from introduced interfaces + * @param array &$interceptedMethods An array (empty or not) which contains the names of the intercepted methods and additional information + * @param array $methodsFromIntroducedInterfaces An array of class and method names from introduced interfaces * @return void */ protected function addIntroducedMethodsToInterceptedMethods(array &$interceptedMethods, array $methodsFromIntroducedInterfaces): void @@ -654,9 +688,9 @@ protected function addIntroducedMethodsToInterceptedMethods(array &$interceptedM * Traverses all aspect containers and returns an array of interface * introductions which match the target class. * - * @param array &$aspectContainers All aspects to take into consideration - * @param string $targetClassName Name of the class the pointcut should match with - * @return array array of interface names + * @param array $aspectContainers All aspects to take into consideration + * @param class-string $targetClassName Name of the class the pointcut should match with + * @return array array of interface names * @throws \Exception */ protected function getMatchingInterfaceIntroductions(array $aspectContainers, string $targetClassName): array @@ -680,9 +714,9 @@ protected function getMatchingInterfaceIntroductions(array $aspectContainers, st * Traverses all aspect containers and returns an array of property * introductions which match the target class. * - * @param array &$aspectContainers All aspects to take into consideration - * @param string $targetClassName Name of the class the pointcut should match with - * @return array|PropertyIntroduction[] array of property introductions + * @param array $aspectContainers All aspects to take into consideration + * @param class-string $targetClassName Name of the class the pointcut should match with + * @return array array of property introductions * @throws \Exception */ protected function getMatchingPropertyIntroductions(array $aspectContainers, string $targetClassName): array @@ -706,24 +740,26 @@ protected function getMatchingPropertyIntroductions(array $aspectContainers, str * Traverses all aspect containers and returns an array of trait * introductions which match the target class. * - * @param array &$aspectContainers All aspects to take into consideration - * @param string $targetClassName Name of the class the pointcut should match with - * @return array array of trait names + * @param array $aspectContainers All aspects to take into consideration + * @param class-string $targetClassName Name of the class the pointcut should match with + * @return array array of trait names * @throws \Exception */ protected function getMatchingTraitNamesFromIntroductions(array $aspectContainers, string $targetClassName): array { $introductions = []; - /** @var AspectContainer $aspectContainer */ foreach ($aspectContainers as $aspectContainer) { if (!$aspectContainer->getCachedTargetClassNameCandidates()->hasClassName($targetClassName)) { continue; } - /** @var TraitIntroduction $introduction */ foreach ($aspectContainer->getTraitIntroductions() as $introduction) { $pointcut = $introduction->getPointcut(); if ($pointcut->matches($targetClassName, null, null, Algorithms::generateRandomString(13))) { - $introductions[] = '\\' . $introduction->getTraitName(); + $traitName = '\\' . $introduction->getTraitName(); + if (!trait_exists($traitName)) { + throw new \InvalidArgumentException('Invalid trait name ' . $traitName, 1744493032); + } + $introductions[] = $traitName; } } } @@ -734,14 +770,18 @@ protected function getMatchingTraitNamesFromIntroductions(array $aspectContainer /** * Returns an array of interface names introduced by the given introductions * - * @param array $interfaceIntroductions An array of interface introductions - * @return array Array of interface names + * @param array $interfaceIntroductions An array of interface introductions + * @return array Array of interface names */ protected function getInterfaceNamesFromIntroductions(array $interfaceIntroductions): array { $interfaceNames = []; foreach ($interfaceIntroductions as $introduction) { - $interfaceNames[] = '\\' . $introduction->getInterfaceName(); + $interfaceName = '\\' . $introduction->getInterfaceName(); + if (!interface_exists($interfaceName)) { + throw new \InvalidArgumentException('Invalid interface name ' . $interfaceName, 1744492473); + } + $interfaceNames[] = $interfaceName; } return $interfaceNames; } @@ -749,8 +789,8 @@ protected function getInterfaceNamesFromIntroductions(array $interfaceIntroducti /** * Returns all methods declared by the introduced interfaces * - * @param array $interfaceIntroductions An array of Aop\InterfaceIntroduction - * @return array An array of method information (interface, method name) + * @param array $interfaceIntroductions An array of Aop\InterfaceIntroduction + * @return array An array of method information (interface, method name) * @throws Aop\Exception */ protected function getIntroducedMethodsFromInterfaceIntroductions(array $interfaceIntroductions): array diff --git a/Neos.Flow/Classes/Aop/InterfaceIntroduction.php b/Neos.Flow/Classes/Aop/InterfaceIntroduction.php index fbbac0183d..a7e6198332 100644 --- a/Neos.Flow/Classes/Aop/InterfaceIntroduction.php +++ b/Neos.Flow/Classes/Aop/InterfaceIntroduction.php @@ -21,13 +21,13 @@ class InterfaceIntroduction { /** * Name of the aspect declaring this introduction - * @var string + * @var class-string */ protected $declaringAspectClassName; /** * Name of the introduced interface - * @var string + * @var class-string */ protected $interfaceName; @@ -40,8 +40,8 @@ class InterfaceIntroduction /** * Constructor * - * @param string $declaringAspectClassName Name of the aspect containing the declaration for this introduction - * @param string $interfaceName Name of the interface to introduce + * @param class-string $declaringAspectClassName Name of the aspect containing the declaration for this introduction + * @param class-string $interfaceName Name of the interface to introduce * @param Pointcut $pointcut The pointcut for this introduction */ public function __construct(string $declaringAspectClassName, string $interfaceName, Pointcut $pointcut) @@ -54,7 +54,7 @@ public function __construct(string $declaringAspectClassName, string $interfaceN /** * Returns the name of the introduced interface * - * @return string Name of the introduced interface + * @return class-string Name of the introduced interface */ public function getInterfaceName(): string { @@ -74,7 +74,7 @@ public function getPointcut(): Pointcut /** * Returns the object name of the aspect which declared this introduction * - * @return string The aspect object name + * @return class-string The aspect object name */ public function getDeclaringAspectClassName(): string { diff --git a/Neos.Flow/Classes/Aop/JoinPoint.php b/Neos.Flow/Classes/Aop/JoinPoint.php index dc1d6e4151..ebe29b006e 100644 --- a/Neos.Flow/Classes/Aop/JoinPoint.php +++ b/Neos.Flow/Classes/Aop/JoinPoint.php @@ -41,13 +41,13 @@ class JoinPoint implements JoinPointInterface /** * Array of method arguments which have been passed to the target method - * @var array + * @var array */ protected $methodArguments; /** * The advice chain for this join point - * @var Advice\AdviceChain + * @var Advice\AdviceChain|null */ protected $adviceChain; @@ -59,7 +59,7 @@ class JoinPoint implements JoinPointInterface /** * The exception thrown (only used for After Throwing advices) - * @var \Exception + * @var \Exception|null */ protected $exception = null; @@ -69,10 +69,10 @@ class JoinPoint implements JoinPointInterface * @param object $proxy Reference to the proxy class instance of the target class * @param string $className Class name of the target class this join point refers to * @param string $methodName Method name of the target method which is about to or has been invoked - * @param array $methodArguments Array of method arguments which have been passed to the target method - * @param Advice\AdviceChain $adviceChain The advice chain for this join point + * @param array $methodArguments Array of method arguments which have been passed to the target method + * @param Advice\AdviceChain|null $adviceChain The advice chain for this join point * @param mixed $result The result of the method invocations (only used for After Returning advices) - * @param \Exception $exception The exception thrown (only used for After Throwing advices) + * @param \Exception|null $exception The exception thrown (only used for After Throwing advices) */ public function __construct($proxy, string $className, string $methodName, array $methodArguments, ?Advice\AdviceChain $adviceChain = null, $result = null, ?\Exception $exception = null) { @@ -121,7 +121,7 @@ public function getMethodName(): string /** * Returns an array of arguments which have been passed to the target method * - * @return array Array of arguments + * @return array Array of arguments * @api */ public function getMethodArguments(): array @@ -178,10 +178,10 @@ public function isMethodArgument($argumentName): bool /** * Returns the advice chain related to this join point * - * @return Advice\AdviceChain The advice chain + * @return ?Advice\AdviceChain The advice chain * @api */ - public function getAdviceChain(): Advice\AdviceChain + public function getAdviceChain(): ?Advice\AdviceChain { return $this->adviceChain; } diff --git a/Neos.Flow/Classes/Aop/JoinPointInterface.php b/Neos.Flow/Classes/Aop/JoinPointInterface.php index 56f27290c1..5718096f91 100644 --- a/Neos.Flow/Classes/Aop/JoinPointInterface.php +++ b/Neos.Flow/Classes/Aop/JoinPointInterface.php @@ -41,7 +41,7 @@ public function getMethodName(); /** * Returns an array of arguments which have been passed to the target method * - * @return array Array of arguments + * @return array Array of arguments */ public function getMethodArguments(); @@ -82,6 +82,7 @@ public function getAdviceChain(); * If an exception was thrown by the target method * Only makes sense for After Throwing advices. * + * @phpstan-assert-if-true !null $this->getException() * @return boolean */ public function hasException(); @@ -91,6 +92,7 @@ public function hasException(); * If no exception has been thrown, NULL is returned. * Only makes sense for After Throwing advices. * + * * @return \Exception|null The exception thrown or NULL */ public function getException(); diff --git a/Neos.Flow/Classes/Aop/Pointcut/Pointcut.php b/Neos.Flow/Classes/Aop/Pointcut/Pointcut.php index 7322b398d2..325552011b 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/Pointcut.php +++ b/Neos.Flow/Classes/Aop/Pointcut/Pointcut.php @@ -41,13 +41,13 @@ class Pointcut implements PointcutFilterInterface /** * If this pointcut is based on a pointcut declaration, contains the name of the aspect class where the pointcut was declared - * @var string + * @var class-string */ - protected $aspectClassName; + protected string $aspectClassName; /** * If this pointcut is based on a pointcut declaration, contains the name of the method acting as the pointcut identifier - * @var string + * @var ?string */ protected $pointcutMethodName; @@ -68,8 +68,8 @@ class Pointcut implements PointcutFilterInterface * * @param string $pointcutExpression A pointcut expression which configures the pointcut * @param PointcutFilterComposite $pointcutFilterComposite - * @param string $aspectClassName The name of the aspect class where the pointcut was declared (either explicitly or from an advice's pointcut expression) - * @param string $pointcutMethodName (optional) If the pointcut is created from a pointcut declaration, the name of the method declaring the pointcut must be passed + * @param class-string $aspectClassName The name of the aspect class where the pointcut was declared (either explicitly or from an advice's pointcut expression) + * @param ?string $pointcutMethodName (optional) If the pointcut is created from a pointcut declaration, the name of the method declaring the pointcut must be passed */ public function __construct(string $pointcutExpression, PointcutFilterComposite $pointcutFilterComposite, string $aspectClassName, ?string $pointcutMethodName = null) { @@ -83,9 +83,9 @@ public function __construct(string $pointcutExpression, PointcutFilterComposite * Checks if the given class and method match this pointcut. * Before each match run, reset() must be called to reset the circular references guard. * - * @param string $className Class to check against - * @param string $methodName Method to check against - * @param string $methodDeclaringClassName Name of the class the method was originally declared in + * @param class-string $className Class to check against + * @param ?string $methodName Method to check against + * @param ?class-string $methodDeclaringClassName Name of the class the method was originally declared in * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection. * @return boolean true if class and method match this point cut, otherwise false * @throws CircularPointcutReferenceException if a circular pointcut reference was detected @@ -119,7 +119,7 @@ public function getPointcutExpression(): string /** * Returns the aspect class name where the pointcut was declared. * - * @return string The aspect class name where the pointcut was declared + * @return class-string The aspect class name where the pointcut was declared */ public function getAspectClassName(): string { @@ -129,9 +129,9 @@ public function getAspectClassName(): string /** * Returns the pointcut method name (if any was defined) * - * @return string The pointcut method name + * @return ?string The pointcut method name */ - public function getPointcutMethodName(): string + public function getPointcutMethodName(): ?string { return $this->pointcutMethodName; } @@ -149,7 +149,7 @@ public function hasRuntimeEvaluationsDefinition(): bool /** * Returns runtime evaluations for the pointcut. * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition(): array { diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutClassAnnotatedWithFilter.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutClassAnnotatedWithFilter.php index aff5b775ed..f979259464 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutClassAnnotatedWithFilter.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutClassAnnotatedWithFilter.php @@ -34,20 +34,20 @@ class PointcutClassAnnotatedWithFilter implements PointcutFilterInterface protected $logger; /** - * @var string A regular expression to match annotations + * @var class-string An annotation class (for example "@Neos\Flow\Annotations\Aspect") which defines which class annotations should match */ protected $annotation; /** - * @var array + * @var array */ protected $annotationValueConstraints; /** * The constructor - initializes the class annotation filter with the expected annotation class * - * @param string $annotation An annotation class (for example "@Neos\Flow\Annotations\Aspect") which defines which class annotations should match - * @param array $annotationValueConstraints + * @param class-string $annotation An annotation class (for example "@Neos\Flow\Annotations\Aspect") which defines which class annotations should match + * @param array $annotationValueConstraints */ public function __construct(string $annotation, array $annotationValueConstraints = []) { @@ -80,9 +80,9 @@ public function injectLogger(LoggerInterface $logger) /** * Checks if the specified class matches with the class tag filter pattern * - * @param string $className Name of the class to check against + * @param class-string $className Name of the class to check against * @param string $methodName Name of the method - not used here - * @param string $methodDeclaringClassName Name of the class the method was originally declared in - not used here + * @param class-string $methodDeclaringClassName Name of the class the method was originally declared in - not used here * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection. * @return bool true if the class matches, otherwise false */ @@ -126,7 +126,7 @@ public function hasRuntimeEvaluationsDefinition(): bool /** * Returns runtime evaluations for the pointcut. * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition(): array { diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutClassNameFilter.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutClassNameFilter.php index a299206c0b..c338a5cd11 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutClassNameFilter.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutClassNameFilter.php @@ -97,7 +97,7 @@ public function hasRuntimeEvaluationsDefinition(): bool /** * Returns runtime evaluations for the pointcut. * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition(): array { diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutClassTypeFilter.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutClassTypeFilter.php index fe0b39a22c..7aea7f3995 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutClassTypeFilter.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutClassTypeFilter.php @@ -30,7 +30,7 @@ class PointcutClassTypeFilter implements PointcutFilterInterface /** * An interface name to match class types - * @var string + * @var class-string */ protected $interfaceOrClassName; @@ -43,7 +43,7 @@ class PointcutClassTypeFilter implements PointcutFilterInterface /** * The constructor - initializes the class type filter with the class or interface name * - * @param string $interfaceOrClassName Interface or a class name to match against + * @param class-string $interfaceOrClassName Interface or a class name to match against * @throws Exception */ public function __construct(string $interfaceOrClassName) @@ -71,19 +71,16 @@ public function injectReflectionService(ReflectionService $reflectionService): v /** * Checks if the specified class matches with the class type filter * - * @param string $className Name of the class to check against + * @param class-string $className Name of the class to check against * @param string $methodName Name of the method - not used here - * @param string $methodDeclaringClassName Name of the class the method was originally declared in - not used here + * @param class-string $methodDeclaringClassName Name of the class the method was originally declared in - not used here * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection. * @return boolean true if the class matches, otherwise false */ public function matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier): bool { - if ($this->isInterface === true) { - return (array_search($this->interfaceOrClassName, class_implements($className)) !== false); - } else { - return ($className === $this->interfaceOrClassName || is_subclass_of($className, $this->interfaceOrClassName)); - } + return $className === $this->interfaceOrClassName + || is_subclass_of($className, $this->interfaceOrClassName); } /** @@ -99,7 +96,7 @@ public function hasRuntimeEvaluationsDefinition(): bool /** * Returns runtime evaluations for the pointcut. * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition(): array { diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutExpressionParser.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutExpressionParser.php index a10cec36d9..64656b7ac3 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutExpressionParser.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutExpressionParser.php @@ -26,6 +26,16 @@ * from a pointcut- or advice annotation and returns a pointcut filter composite. * * @see \Neos\Flow\Aop\Pointcut, PointcutFilterComposite + * + * @phpstan-type ArgumentConstraints array, + * value: array, + * }> + * @phpstan-type RuntimeEvaluationCondition array{ + * operator: string, + * leftValue: string, + * rightValue: mixed, + * } * @Flow\Scope("singleton") * @Flow\Proxy(false) */ @@ -123,11 +133,11 @@ public function parse(string $pointcutExpression, string $sourceHint): PointcutF { $this->sourceHint = $sourceHint; - if (!is_string($pointcutExpression) || strlen($pointcutExpression) === 0) { + if (strlen($pointcutExpression) === 0) { throw new InvalidPointcutExpressionException(sprintf('Pointcut expression must be a valid string, "%s" given, defined in "%s"', gettype($pointcutExpression), $this->sourceHint), 1168874738); } $pointcutFilterComposite = new PointcutFilterComposite(); - $pointcutExpressionParts = preg_split(self::PATTERN_SPLITBYOPERATOR, $pointcutExpression, -1, PREG_SPLIT_DELIM_CAPTURE); + $pointcutExpressionParts = preg_split(self::PATTERN_SPLITBYOPERATOR, $pointcutExpression, -1, PREG_SPLIT_DELIM_CAPTURE) ?: []; $count = count($pointcutExpressionParts); for ($partIndex = 0; $partIndex < $count; $partIndex += 2) { @@ -158,6 +168,7 @@ public function parse(string $pointcutExpression, string $sourceHint): PointcutF case 'filter': case 'setting': $parseMethodName = 'parseDesignator' . ucfirst($pointcutDesignator); + /** @phpstan-ignore argument.type, argument.type, argument.type (I guess we excluded empty string for class names here, already) */ $this->$parseMethodName($operator, $signaturePattern, $pointcutFilterComposite); break; case 'evaluate': @@ -176,7 +187,7 @@ public function parse(string $pointcutExpression, string $sourceHint): PointcutF * filter composite object. * * @param string $operator The operator - * @param string $annotationPattern The pattern expression as configuration for the class annotation filter + * @param class-string $annotationPattern The pattern expression as configuration for the class annotation filter * @param PointcutFilterComposite $pointcutFilterComposite An instance of the pointcut filter composite. The result (ie. the class annotation filter) will be added to this composite object. * @return void */ @@ -184,6 +195,9 @@ protected function parseDesignatorClassAnnotatedWith(string $operator, string $a { $annotationPropertyConstraints = []; $this->parseAnnotationPattern($annotationPattern, $annotationPropertyConstraints); + if (!class_exists($annotationPattern)) { + throw new \InvalidArgumentException('Cannot resolve annotation pattern class ' . $annotationPattern, 1744491903); + } $filter = new PointcutClassAnnotatedWithFilter($annotationPattern, $annotationPropertyConstraints); $filter->injectReflectionService($this->reflectionService); @@ -212,7 +226,7 @@ protected function parseDesignatorClass(string $operator, string $classPattern, * filter composite object. * * @param string $operator The operator - * @param string $annotationPattern The pattern expression as configuration for the method annotation filter + * @param class-string $annotationPattern The pattern expression as configuration for the method annotation filter * @param PointcutFilterComposite $pointcutFilterComposite An instance of the pointcut filter composite. The result (ie. the method annotation filter) will be added to this composite object. * @return void */ @@ -221,6 +235,9 @@ protected function parseDesignatorMethodAnnotatedWith(string $operator, string $ $annotationPropertyConstraints = []; $this->parseAnnotationPattern($annotationPattern, $annotationPropertyConstraints); + if (!class_exists($annotationPattern)) { + throw new \InvalidArgumentException('Invalid annotation pattern ' . $annotationPattern . ', must be an existing class', 1744490385); + } $filter = new PointcutMethodAnnotatedWithFilter($annotationPattern, $annotationPropertyConstraints); $filter->injectReflectionService($this->reflectionService); $filter->injectLogger($this->objectManager->get(PsrLoggerFactoryInterface::class)->get('systemLogger')); @@ -232,7 +249,8 @@ protected function parseDesignatorMethodAnnotatedWith(string $operator, string $ * needed. * * @param string $annotationPattern - * @param array $annotationPropertyConstraints + * @param array $annotationPropertyConstraints + * * @return void */ protected function parseAnnotationPattern(string &$annotationPattern, array &$annotationPropertyConstraints): void @@ -241,8 +259,8 @@ protected function parseAnnotationPattern(string &$annotationPattern, array &$an $matches = []; preg_match(self::PATTERN_MATCHMETHODNAMEANDARGUMENTS, $annotationPattern, $matches); - $annotationPattern = $matches['MethodName']; - $annotationPropertiesPattern = $matches['MethodArguments']; + $annotationPattern = $matches['MethodName'] ?? ''; + $annotationPropertiesPattern = $matches['MethodArguments'] ?? ''; $annotationPropertyConstraints = $this->getArgumentConstraintsFromMethodArgumentsPattern($annotationPropertiesPattern); } } @@ -272,8 +290,8 @@ protected function parseDesignatorMethod(string $operator, string $signaturePatt $matches = []; preg_match(self::PATTERN_MATCHMETHODNAMEANDARGUMENTS, $methodPattern, $matches); - $methodNamePattern = $matches['MethodName']; - $methodArgumentPattern = $matches['MethodArguments']; + $methodNamePattern = $matches['MethodName'] ?? ''; + $methodArgumentPattern = $matches['MethodArguments'] ?? ''; $methodArgumentConstraints = $this->getArgumentConstraintsFromMethodArgumentsPattern($methodArgumentPattern); $classNameFilter = new PointcutClassNameFilter($classPattern); @@ -299,8 +317,9 @@ protected function parseDesignatorMethod(string $operator, string $signaturePatt * Adds a class type filter to the pointcut filter composite * * @param string $operator - * @param string $signaturePattern The pattern expression defining the class type + * @param class-string $signaturePattern The pattern expression defining the class type * @param PointcutFilterComposite $pointcutFilterComposite An instance of the pointcut filter composite. The result (ie. the class type filter) will be added to this composite object. + * @todo this is only ever called from tests, do we even need this? * @return void */ protected function parseDesignatorWithin(string $operator, string $signaturePattern, PointcutFilterComposite $pointcutFilterComposite): void @@ -452,7 +471,7 @@ protected function getVisibilityFromSignaturePattern(string &$signaturePattern) * Parses the method arguments pattern and returns the corresponding constraints array * * @param string $methodArgumentsPattern The arguments pattern defined in the pointcut expression - * @return array The corresponding constraints array + * @return ArgumentConstraints The corresponding constraints array */ protected function getArgumentConstraintsFromMethodArgumentsPattern(string $methodArgumentsPattern): array { @@ -467,7 +486,7 @@ protected function getArgumentConstraintsFromMethodArgumentsPattern(string $meth $list = []; $listEntries = []; - if (preg_match('/^\s*\(.*\)\s*$/', $matches[3][$i], $list) > 0) { + if (is_string($matches[3][$i]) && preg_match('/^\s*\(.*\)\s*$/', $matches[3][$i], $list) > 0) { preg_match_all(self::PATTERN_MATCHRUNTIMEEVALUATIONSVALUELIST, $list[0], $listEntries); $matches[3][$i] = $listEntries[1]; } @@ -483,7 +502,7 @@ protected function getArgumentConstraintsFromMethodArgumentsPattern(string $meth * Parses the evaluate string for runtime evaluations and returns the corresponding conditions array * * @param string $evaluateString The evaluate string defined in the pointcut expression - * @return array The corresponding constraints array + * @return array The corresponding constraints array */ protected function getRuntimeEvaluationConditionsFromEvaluateString(string $evaluateString): array { @@ -498,7 +517,7 @@ protected function getRuntimeEvaluationConditionsFromEvaluateString(string $eval $list = []; $listEntries = []; - if (preg_match('/^\s*\(.*\)\s*$/', $matches[3][$i], $list) > 0) { + if (is_string($matches[3][$i]) && preg_match('/^\s*\(.*\)\s*$/', $matches[3][$i], $list) > 0) { preg_match_all(self::PATTERN_MATCHRUNTIMEEVALUATIONSVALUELIST, $list[0], $listEntries); $matches[3][$i] = $listEntries[1]; } diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutFilter.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutFilter.php index 059819d8b8..dd258bce43 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutFilter.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutFilter.php @@ -73,9 +73,9 @@ public function injectProxyClassBuilder(ProxyClassBuilder $proxyClassBuilder): v /** * Checks if the specified class and method matches with the pointcut * - * @param string $className Name of the class to check against + * @param class-string $className Name of the class to check against * @param string $methodName Name of the method - not used here - * @param string $methodDeclaringClassName Name of the class the method was originally declared in + * @param class-string $methodDeclaringClassName Name of the class the method was originally declared in * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection. * @return boolean true if the class matches, otherwise false * @throws UnknownPointcutException @@ -104,7 +104,7 @@ public function hasRuntimeEvaluationsDefinition(): bool /** * Returns runtime evaluations for the pointcut. * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition(): array { diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutFilterComposite.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutFilterComposite.php index 699e72df63..0279d0bd5b 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutFilterComposite.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutFilterComposite.php @@ -27,7 +27,7 @@ class PointcutFilterComposite implements PointcutFilterInterface { /** - * @var array An array of \Neos\Flow\Aop\Pointcut\Pointcut*Filter objects + * @var array An array of \Neos\Flow\Aop\Pointcut\Pointcut*Filter objects */ protected $filters = []; @@ -37,12 +37,12 @@ class PointcutFilterComposite implements PointcutFilterInterface protected $earlyReturn = true; /** - * @var array An array of runtime evaluations + * @var array An array of runtime evaluations */ protected $runtimeEvaluationsDefinition = []; /** - * @var array An array of global runtime evaluations + * @var array An array of global runtime evaluations */ protected $globalRuntimeEvaluationsDefinition = []; @@ -50,9 +50,9 @@ class PointcutFilterComposite implements PointcutFilterInterface * Checks if the specified class and method match the registered class- * and method filter patterns. * - * @param string $className Name of the class to check against - * @param string $methodName Name of the method to check against - * @param string $methodDeclaringClassName Name of the class the method was originally declared in + * @param class-string $className Name of the class to check against + * @param ?string $methodName Name of the method to check against + * @param ?class-string $methodDeclaringClassName Name of the class the method was originally declared in * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection. * @return boolean true if class and method match the pattern, otherwise false */ @@ -143,7 +143,7 @@ public function hasRuntimeEvaluationsDefinition() /** * Returns runtime evaluations for the pointcut. * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition() { @@ -154,7 +154,7 @@ public function getRuntimeEvaluationsDefinition() * Sets static runtime evaluations for to pointcut, that will be used for every * method this composite matches * - * @param array $runtimeEvaluations Runtime evaluations to be added + * @param array $runtimeEvaluations Runtime evaluations to be added * @return void */ public function setGlobalRuntimeEvaluationsDefinition(array $runtimeEvaluations): void @@ -214,7 +214,7 @@ public function reduceTargetClassNames(ClassNameIndex $classNameIndex) * Returns the PHP code of the conditions used for runtime evaluations * * @param string $operator The operator for the given condition - * @param array $conditions Condition array + * @param array $conditions Condition array * @param boolean &$useGlobalObjects Set to true if global objects are used by the condition * @return string The condition code */ @@ -289,7 +289,10 @@ protected function buildRuntimeEvaluationsConditionCode($operator, array $condit /** * Returns the PHP code of the conditions used argument runtime evaluations * - * @param array $conditions Condition array + * @param array, + * value: array, + * }> $conditions Condition array * @param boolean &$useGlobalObjects Set to true if global objects are used by the condition * @return string The arguments condition code */ @@ -329,7 +332,11 @@ protected function buildMethodArgumentsEvaluationConditionCode(array $conditions /** * Returns the PHP code of the conditions used for global runtime evaluations * - * @param array $conditions Condition array + * @param array $conditions Condition array * @param boolean &$useGlobalObjects Set to true if global objects are used by the condition * @return string The condition code */ diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutFilterInterface.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutFilterInterface.php index fdebcdfd95..2464fae7b2 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutFilterInterface.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutFilterInterface.php @@ -24,9 +24,9 @@ interface PointcutFilterInterface /** * Checks if the specified class and method matches against the filter * - * @param string $className Name of the class to check against + * @param class-string $className Name of the class to check against * @param string $methodName Name of the method to check against - * @param string $methodDeclaringClassName Name of the class the method was originally declared in + * @param class-string $methodDeclaringClassName Name of the class the method was originally declared in * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection. * @return boolean true if the class / method match, otherwise false */ @@ -42,7 +42,7 @@ public function hasRuntimeEvaluationsDefinition(); /** * Returns runtime evaluations for a previously matched pointcut * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition(); diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutMethodAnnotatedWithFilter.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutMethodAnnotatedWithFilter.php index ba44576a91..7a121d1ee0 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutMethodAnnotatedWithFilter.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutMethodAnnotatedWithFilter.php @@ -33,22 +33,21 @@ class PointcutMethodAnnotatedWithFilter implements PointcutFilterInterface */ protected $logger; - /** - * @var string The tag of an annotation to match against + * @var class-string The tag of an annotation to match against */ - protected $annotation; + protected string $annotation; /** - * @var array + * @var array */ protected $annotationValueConstraints; /** * The constructor - initializes the method annotation filter with the expected annotation class * - * @param string $annotation An annotation class (for example "Neos\Flow\Annotations\Lazy") which defines which method annotations should match - * @param array $annotationValueConstraints + * @param class-string $annotation An annotation class (for example "Neos\Flow\Annotations\Lazy") which defines which method annotations should match + * @param array $annotationValueConstraints */ public function __construct(string $annotation, array $annotationValueConstraints = []) { @@ -81,15 +80,15 @@ public function injectLogger(LoggerInterface $logger) /** * Checks if the specified method matches with the method annotation filter pattern * - * @param string $className Name of the class to check against - not used here + * @param class-string $className Name of the class to check against - not used here * @param string $methodName Name of the method - * @param string $methodDeclaringClassName Name of the class the method was originally declared in + * @param class-string $methodDeclaringClassName Name of the class the method was originally declared in * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection - not used here * @return boolean true if the class matches, otherwise false */ public function matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier): bool { - if ($methodDeclaringClassName === null || !method_exists($methodDeclaringClassName, $methodName)) { + if (!method_exists($methodDeclaringClassName, $methodName)) { return false; } @@ -128,7 +127,7 @@ public function hasRuntimeEvaluationsDefinition(): bool /** * Returns runtime evaluations for the pointcut. * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition(): array { diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutMethodNameFilter.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutMethodNameFilter.php index 0c03d3ef2e..6bad8e9f21 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutMethodNameFilter.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutMethodNameFilter.php @@ -48,7 +48,7 @@ class PointcutMethodNameFilter implements PointcutFilterInterface protected $logger; /** - * @var array Array with constraints for method arguments + * @var array Array with constraints for method arguments */ protected $methodArgumentConstraints = []; @@ -57,7 +57,7 @@ class PointcutMethodNameFilter implements PointcutFilterInterface * * @param string $methodNameFilterExpression A regular expression which filters method names * @param string|null $methodVisibility The method visibility modifier (public, protected or private). Specify NULL if you don't care. - * @param array $methodArgumentConstraints array of method constraints + * @param array $methodArgumentConstraints array of method constraints * @throws InvalidPointcutExpressionException */ public function __construct(string $methodNameFilterExpression, ?string $methodVisibility = null, array $methodArgumentConstraints = []) @@ -98,9 +98,9 @@ public function injectLogger(LoggerInterface $logger) * * Returns true if method name, visibility and arguments constraints match. * - * @param string $className Ignored in this pointcut filter + * @param class-string $className Ignored in this pointcut filter * @param string $methodName Name of the method to match against - * @param string $methodDeclaringClassName Name of the class the method was originally declared in + * @param class-string $methodDeclaringClassName Name of the class the method was originally declared in * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection. * @return boolean true if the class matches, otherwise false * @throws Exception @@ -117,18 +117,18 @@ public function matches($className, $methodName, $methodDeclaringClassName, $poi switch ($this->methodVisibility) { case 'public': - if (!($methodDeclaringClassName !== null && $this->reflectionService->isMethodPublic($methodDeclaringClassName, $methodName))) { + if (!($this->reflectionService->isMethodPublic($methodDeclaringClassName, $methodName))) { return false; } break; case 'protected': - if (!($methodDeclaringClassName !== null && $this->reflectionService->isMethodProtected($methodDeclaringClassName, $methodName))) { + if (!($this->reflectionService->isMethodProtected($methodDeclaringClassName, $methodName))) { return false; } break; } - $methodArguments = ($methodDeclaringClassName === null ? [] : $this->reflectionService->getMethodParameters($methodDeclaringClassName, $methodName)); + $methodArguments = $this->reflectionService->getMethodParameters($methodDeclaringClassName, $methodName); foreach (array_keys($this->methodArgumentConstraints) as $argumentName) { $objectAccess = explode('.', $argumentName, 2); $argumentName = $objectAccess[0]; @@ -153,7 +153,7 @@ public function hasRuntimeEvaluationsDefinition(): bool /** * Returns runtime evaluations for a previously matched pointcut * - * @return array Runtime evaluations + * @return array{methodArgumentConstraints: array} Runtime evaluations */ public function getRuntimeEvaluationsDefinition(): array { @@ -185,7 +185,7 @@ public function getMethodVisibility(): ?string /** * Returns the method argument constraints * - * @return array + * @return array */ public function getMethodArgumentConstraints(): array { diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutSettingFilter.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutSettingFilter.php index ec0f390f95..ef2a26a60e 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutSettingFilter.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutSettingFilter.php @@ -109,7 +109,7 @@ public function hasRuntimeEvaluationsDefinition(): bool /** * Returns runtime evaluations for the pointcut. * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition(): array { @@ -126,7 +126,7 @@ public function getRuntimeEvaluationsDefinition(): array */ protected function parseConfigurationOptionPath(string $settingComparisonExpression): void { - $settingComparisonExpression = preg_split(self::PATTERN_SPLITBYEQUALSIGN, $settingComparisonExpression); + $settingComparisonExpression = preg_split(self::PATTERN_SPLITBYEQUALSIGN, $settingComparisonExpression) ?: []; if (isset($settingComparisonExpression[1])) { $matches = []; preg_match(self::PATTERN_MATCHVALUEINQUOTES, $settingComparisonExpression[1], $matches); @@ -141,6 +141,7 @@ protected function parseConfigurationOptionPath(string $settingComparisonExpress $configurationKeys = explode('.', $settingComparisonExpression[0]); + /** @phpstan-ignore greater.alwaysTrue (not sure about this) */ if (count($configurationKeys) > 0) { $settingPackageKey = array_shift($configurationKeys); $settingValue = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, $settingPackageKey); diff --git a/Neos.Flow/Classes/Aop/Pointcut/RuntimeExpressionEvaluator.php b/Neos.Flow/Classes/Aop/Pointcut/RuntimeExpressionEvaluator.php index ffd7b3483e..decb3660b9 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/RuntimeExpressionEvaluator.php +++ b/Neos.Flow/Classes/Aop/Pointcut/RuntimeExpressionEvaluator.php @@ -40,7 +40,7 @@ class RuntimeExpressionEvaluator /** * Currently existing runtime expressions loaded from cache. * - * @var array + * @var array */ protected $runtimeExpressions = []; diff --git a/Neos.Flow/Classes/Aop/PropertyIntroduction.php b/Neos.Flow/Classes/Aop/PropertyIntroduction.php index 556e512070..c669800b51 100644 --- a/Neos.Flow/Classes/Aop/PropertyIntroduction.php +++ b/Neos.Flow/Classes/Aop/PropertyIntroduction.php @@ -21,7 +21,7 @@ class PropertyIntroduction { /** * Name of the aspect declaring this introduction - * @var string + * @var class-string */ protected $declaringAspectClassName; @@ -58,7 +58,7 @@ class PropertyIntroduction /** * Constructor * - * @param string $declaringAspectClassName Name of the aspect containing the declaration for this introduction + * @param class-string $declaringAspectClassName Name of the aspect containing the declaration for this introduction * @param string $propertyName Name of the property to introduce * @param Pointcut $pointcut The pointcut for this introduction */ @@ -80,7 +80,7 @@ public function __construct(string $declaringAspectClassName, string $propertyNa } else { $this->propertyVisibility = 'public'; } - $this->propertyDocComment = preg_replace('/@(Neos\\\\Flow\\\\Annotations|Flow)\\\\Introduce.+$/mi', 'introduced by ' . $declaringAspectClassName, $propertyReflection->getDocComment()); + $this->propertyDocComment = preg_replace('/@(Neos\\\\Flow\\\\Annotations|Flow)\\\\Introduce.+$/mi', 'introduced by ' . $declaringAspectClassName, $propertyReflection->getDocComment() ?: '') ?: ''; } /** @@ -132,9 +132,9 @@ public function getPointcut(): Pointcut } /** - * Returns the object name of the aspect which declared this introduction + * Returns the class name of the aspect which declared this introduction * - * @return string The aspect object name + * @return class-string The aspect class name */ public function getDeclaringAspectClassName(): string { diff --git a/Neos.Flow/Classes/Aop/TraitIntroduction.php b/Neos.Flow/Classes/Aop/TraitIntroduction.php index b9adfd6c7b..9443fde57a 100644 --- a/Neos.Flow/Classes/Aop/TraitIntroduction.php +++ b/Neos.Flow/Classes/Aop/TraitIntroduction.php @@ -21,13 +21,13 @@ class TraitIntroduction { /** * Name of the aspect declaring this introduction - * @var string + * @var class-string */ protected $declaringAspectClassName; /** * Name of the introduced trait - * @var string + * @var trait-string */ protected $traitName; @@ -41,8 +41,8 @@ class TraitIntroduction /** * Constructor * - * @param string $declaringAspectClassName Name of the aspect containing the declaration for this introduction - * @param string $traitName Name of the trait to introduce + * @param class-string $declaringAspectClassName Name of the aspect containing the declaration for this introduction + * @param trait-string $traitName Name of the trait to introduce * @param Pointcut $pointcut The pointcut for this introduction */ public function __construct(string $declaringAspectClassName, string $traitName, Pointcut $pointcut) @@ -55,7 +55,7 @@ public function __construct(string $declaringAspectClassName, string $traitName, /** * Returns the name of the introduced trait * - * @return string Name of the introduced trait + * @return trait-string Name of the introduced trait */ public function getTraitName(): string { @@ -75,7 +75,7 @@ public function getPointcut(): Pointcut /** * Returns the object name of the aspect which declared this introduction * - * @return string The aspect object name + * @return class-string The aspect object name */ public function getDeclaringAspectClassName(): string { diff --git a/Neos.Flow/Classes/Cache/AnnotationsCacheFlusher.php b/Neos.Flow/Classes/Cache/AnnotationsCacheFlusher.php index 60616e3916..c143a6f841 100644 --- a/Neos.Flow/Classes/Cache/AnnotationsCacheFlusher.php +++ b/Neos.Flow/Classes/Cache/AnnotationsCacheFlusher.php @@ -54,15 +54,15 @@ final class AnnotationsCacheFlusher /** * Caches to flush for a given annotation * - * @var array in the format [ => [, ]] + * @var array> in the format [ => [, ]] */ private $annotationToCachesMap = []; /** * Register an annotation that should trigger a cache flush * - * @param string $annotationClassName fully qualified class name of the annotation - * @param string[] $cacheNames Cache names to flush if a class containing the given annotation is compiled (e.g. ["Flow_Mvc_Routing_Route", Flow_Mvc_Routing_Resolve"]) + * @param class-string $annotationClassName fully qualified class name of the annotation + * @param list $cacheNames Cache names to flush if a class containing the given annotation is compiled (e.g. ["Flow_Mvc_Routing_Route", Flow_Mvc_Routing_Resolve"]) */ public function registerAnnotation(string $annotationClassName, array $cacheNames): void { @@ -72,7 +72,7 @@ public function registerAnnotation(string $annotationClassName, array $cacheName /** * A slot that flushes caches as needed if classes with specific annotations have changed @see registerAnnotation() * - * @param array $classNames The full class names of the classes that got compiled + * @param array $classNames The full class names of the classes that got compiled * @return void * @throws NoSuchCacheException */ diff --git a/Neos.Flow/Classes/Cache/CacheFactory.php b/Neos.Flow/Classes/Cache/CacheFactory.php index 1b2f885775..3f7537c155 100644 --- a/Neos.Flow/Classes/Cache/CacheFactory.php +++ b/Neos.Flow/Classes/Cache/CacheFactory.php @@ -83,7 +83,7 @@ public function __construct(ApplicationContext $context, Environment $environmen * @param string $cacheIdentifier * @param string $cacheObjectName * @param string $backendObjectName - * @param array $backendOptions + * @param array $backendOptions * @param bool $persistent * @return FrontendInterface */ @@ -112,7 +112,7 @@ protected function instantiateCache(string $cacheIdentifier, string $cacheObject /** * @param string $backendObjectName - * @param array $backendOptions + * @param array $backendOptions * @param EnvironmentConfiguration $environmentConfiguration * @param boolean $persistent * @return BackendInterface diff --git a/Neos.Flow/Classes/Cache/CacheManager.php b/Neos.Flow/Classes/Cache/CacheManager.php index b4e6b4c6d6..78a1ccbf2c 100644 --- a/Neos.Flow/Classes/Cache/CacheManager.php +++ b/Neos.Flow/Classes/Cache/CacheManager.php @@ -11,6 +11,7 @@ * source code. */ +use Neos\Cache\Backend\BackendInterface; use Neos\Cache\CacheFactoryInterface; use Neos\Flow\Annotations as Flow; use Neos\Cache\Backend\FileBackend; @@ -32,6 +33,12 @@ /** * The Cache Manager * + * @phpstan-type CacheConfiguration array{ + * frontend?: class-string, + * backend?: class-string, + * backendOptions?: array, + * persistent?: bool, + * } * @Flow\Scope("singleton") * @api */ @@ -73,12 +80,12 @@ class CacheManager protected $cacheItemPools = []; /** - * @var array + * @var array */ protected $persistentCaches = []; /** - * @var array + * @var array */ protected $cacheConfigurations = [ 'Default' => [ @@ -140,16 +147,13 @@ public function injectEnvironment(Environment $environment): void * If one of the options is not specified, the default value is assumed. * Existing cache configurations are preserved. * - * @param array $cacheConfigurations The cache configurations to set + * @todo enforce via extractor + * @param array $cacheConfigurations The cache configurations to set * @return void - * @throws \InvalidArgumentException */ public function setCacheConfigurations(array $cacheConfigurations): void { foreach ($cacheConfigurations as $identifier => $configuration) { - if (!is_array($configuration)) { - throw new \InvalidArgumentException('The cache configuration for cache "' . $identifier . '" was not an array as expected.', 1231259656); - } $this->cacheConfigurations[$identifier] = $configuration; } } @@ -301,7 +305,7 @@ public function flushCachesByTag(string $tag, bool $flushPersistentCaches = fals /** * Returns an array of cache configurations, indexed by cache identifier * - * @return array + * @return array */ public function getCacheConfigurations(): array { @@ -320,7 +324,7 @@ public function getCacheConfigurations(): array * time needed. * * @param string $fileMonitorIdentifier Identifier of the File Monitor - * @param array $changedFiles A list of full paths to changed files + * @param array $changedFiles A list of full paths to changed files * @return void */ public function flushSystemCachesByChangedFiles(string $fileMonitorIdentifier, array $changedFiles): void @@ -341,7 +345,7 @@ public function flushSystemCachesByChangedFiles(string $fileMonitorIdentifier, a /** * Flushes entries tagged with class names if their class source files have changed. * - * @param array $changedFiles A list of full paths to changed files + * @param array $changedFiles A list of full paths to changed files * @return void * @see flushSystemCachesByChangedFiles() */ @@ -355,7 +359,7 @@ protected function flushClassCachesByChangedFiles(array $changedFiles): void if (!file_exists($pathAndFilename)) { continue; } - $fileContents = file_get_contents($pathAndFilename); + $fileContents = file_get_contents($pathAndFilename) ?: ''; $className = (new PhpAnalyzer($fileContents))->extractFullyQualifiedClassName(); if ($className === null) { continue; @@ -408,7 +412,7 @@ protected function flushClassCachesByChangedFiles(array $changedFiles): void /** * Flushes caches as needed if settings, routes or policies have changed * - * @param array $changedFiles A list of full paths to changed files + * @param array $changedFiles A list of full paths to changed files * @return void * @see flushSystemCachesByChangedFiles() */ @@ -458,7 +462,7 @@ protected function flushConfigurationCachesByChangedFiles(array $changedFiles): /** * Flushes I18n caches if translation files have changed * - * @param array $changedFiles A list of full paths to changed files + * @param array $changedFiles A list of full paths to changed files * @return void * @see flushSystemCachesByChangedFiles() */ @@ -495,11 +499,15 @@ protected function createAllCaches(): void */ protected function createCache(string $identifier): void { - $frontend = isset($this->cacheConfigurations[$identifier]['frontend']) ? $this->cacheConfigurations[$identifier]['frontend'] : $this->cacheConfigurations['Default']['frontend']; - $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 + /** @phpstan-ignore offsetAccess.notFound (always set for Default) */ + $frontend = $this->cacheConfigurations[$identifier]['frontend'] ?? $this->cacheConfigurations['Default']['frontend']; + /** @phpstan-ignore offsetAccess.notFound (always set for Default) */ + $backend = $this->cacheConfigurations[$identifier]['backend'] ?? $this->cacheConfigurations['Default']['backend']; + /** @phpstan-ignore offsetAccess.notFound (always set for Default) */ + $backendOptions = $this->cacheConfigurations[$identifier]['backendOptions'] ?? $this->cacheConfigurations['Default']['backendOptions']; + /** @phpstan-ignore offsetAccess.notFound (always set for Default) */ + $persistent = $this->cacheConfigurations[$identifier]['persistent'] ?? $this->cacheConfigurations['Default']['persistent']; + /** @phpstan-ignore arguments.count ($persistent is not part of the interface) */ $cache = $this->cacheFactory->create($identifier, $frontend, $backend, $backendOptions, $persistent); $this->registerCache($cache, $persistent); } diff --git a/Neos.Flow/Classes/Cli/Command.php b/Neos.Flow/Classes/Cli/Command.php index 07f8e818ec..57f3727338 100644 --- a/Neos.Flow/Classes/Cli/Command.php +++ b/Neos.Flow/Classes/Cli/Command.php @@ -22,7 +22,7 @@ class Command { /** - * @var string + * @var class-string */ protected $controllerClassName; @@ -54,7 +54,7 @@ class Command /** * Constructor * - * @param string $controllerClassName Class name of the controller providing the command + * @param class-string $controllerClassName Class name of the controller providing the command * @param string $controllerCommandName Command name, i.e. the method name of the command, without the "Command" suffix * @throws \InvalidArgumentException */ @@ -74,7 +74,7 @@ public function __construct(string $controllerClassName, string $controllerComma /** * @param ReflectionService $reflectionService Reflection service */ - public function injectReflectionService(ReflectionService $reflectionService) + public function injectReflectionService(ReflectionService $reflectionService): void { $this->reflectionService = $reflectionService; } @@ -82,13 +82,13 @@ public function injectReflectionService(ReflectionService $reflectionService) /** * @param ObjectManagerInterface $objectManager */ - public function injectObjectManager(ObjectManagerInterface $objectManager) + public function injectObjectManager(ObjectManagerInterface $objectManager): void { $this->objectManager = $objectManager; } /** - * @return string + * @return class-string */ public function getControllerClassName(): string { @@ -122,10 +122,16 @@ public function getShortDescription(): string { $commandMethodReflection = $this->getCommandMethodReflection(); $lines = explode(chr(10), $commandMethodReflection->getDescription()); - $shortDescription = ((count($lines) > 0) ? trim($lines[0]) : '') . ($this->isDeprecated() ? ' (DEPRECATED)' : ''); + /** @phpstan-ignore greater.alwaysTrue (not sure about this) */ + $shortDescription = ((count($lines) > 0) + ? trim($lines[0]) + : '') . ($this->isDeprecated() ? ' (DEPRECATED)' : ''); if ($commandMethodReflection->getDeclaringClass()->implementsInterface(DescriptionAwareCommandControllerInterface::class)) { - $shortDescription = call_user_func([$this->controllerClassName, 'processDescription'], $this->controllerCommandName, $shortDescription, $this->objectManager); + $callback = [$this->controllerClassName, 'processDescription']; + if (is_callable($callback)) { + $shortDescription = call_user_func($callback, $this->controllerCommandName, $shortDescription, $this->objectManager); + } } return $shortDescription; } @@ -151,7 +157,10 @@ public function getDescription(): string } $description = implode(chr(10), $descriptionLines); if ($commandMethodReflection->getDeclaringClass()->implementsInterface(DescriptionAwareCommandControllerInterface::class)) { - $description = call_user_func([$this->controllerClassName, 'processDescription'], $this->controllerCommandName, $description, $this->objectManager); + $callback = [$this->controllerClassName, 'processDescription']; + if (is_callable($callback)) { + $description = call_user_func($callback, $this->controllerCommandName, $description, $this->objectManager); + } } return $description; } @@ -239,7 +248,7 @@ public function isFlushingCaches(): bool * Returns an array of command identifiers which were specified in the "@see" * annotation of a command method. * - * @return array + * @return array */ public function getRelatedCommandIdentifiers(): array { diff --git a/Neos.Flow/Classes/Cli/CommandArgumentDefinition.php b/Neos.Flow/Classes/Cli/CommandArgumentDefinition.php index f912d6e34a..44495b5c65 100644 --- a/Neos.Flow/Classes/Cli/CommandArgumentDefinition.php +++ b/Neos.Flow/Classes/Cli/CommandArgumentDefinition.php @@ -62,7 +62,7 @@ public function getName(): string public function getDashedName(): string { $dashedName = ucfirst($this->name); - $dashedName = preg_replace('/([A-Z][a-z0-9]+)/', '$1-', $dashedName); + $dashedName = preg_replace('/([A-Z][a-z0-9]+)/', '$1-', $dashedName) ?: ''; return '--' . strtolower(substr($dashedName, 0, -1)); } diff --git a/Neos.Flow/Classes/Cli/CommandController.php b/Neos.Flow/Classes/Cli/CommandController.php index 39e590a53d..7a5a09dea9 100644 --- a/Neos.Flow/Classes/Cli/CommandController.php +++ b/Neos.Flow/Classes/Cli/CommandController.php @@ -12,10 +12,8 @@ */ use Neos\Flow\Annotations as Flow; -use Neos\Flow\Command\HelpCommandController; use Neos\Flow\Mvc\Controller\Argument; use Neos\Flow\Mvc\Controller\Arguments; -use Neos\Flow\Mvc\Exception\CommandException; use Neos\Flow\Mvc\Exception\InvalidArgumentTypeException; use Neos\Flow\Mvc\Exception\NoSuchCommandException; use Neos\Flow\Cli\Exception\StopCommandException; @@ -189,10 +187,6 @@ protected function mapRequestArgumentsToControllerArguments() $argumentValue = $this->output->ask(sprintf('Please specify the required argument "%s": ', $argumentName)); } - if ($argumentValue === null) { - $exception = new CommandException(sprintf('Required argument "%s" is not set.', $argumentName), 1306755520); - $this->forward('error', HelpCommandController::class, ['exception' => $exception]); - } $argument->setValue($argumentValue); } } @@ -204,8 +198,8 @@ protected function mapRequestArgumentsToControllerArguments() * without the need for a new request. * * @param string $commandName - * @param string $controllerObjectName - * @param array $arguments + * @param ?string $controllerObjectName + * @param array $arguments * @return void * @throws StopCommandException */ @@ -213,7 +207,11 @@ protected function forward(string $commandName, ?string $controllerObjectName = { $this->request->setDispatched(false); $this->request->setControllerCommandName($commandName); - if ($controllerObjectName !== null) { + if ( + $controllerObjectName !== null + && class_exists($controllerObjectName) + && is_subclass_of($controllerObjectName, CommandControllerInterface::class) + ) { $this->request->setControllerObjectName($controllerObjectName); } $this->request->setArguments($arguments); @@ -284,7 +282,7 @@ public function getFlowInvocationString(): string * @see http://www.php.net/sprintf * * @param string $text Text to output - * @param array $arguments Optional arguments to use for sprintf + * @param array $arguments Optional arguments to use for sprintf * @return void * @api */ @@ -297,7 +295,7 @@ protected function output(string $text, array $arguments = []) * Outputs specified text to the console window and appends a line break * * @param string $text Text to output - * @param array $arguments Optional arguments to use for sprintf + * @param array $arguments Optional arguments to use for sprintf * @return void * @see output() * @see outputLines() @@ -313,7 +311,7 @@ protected function outputLine(string $text = '', array $arguments = []) * console window * * @param string $text Text to output - * @param array $arguments Optional arguments to use for sprintf + * @param array $arguments Optional arguments to use for sprintf * @param integer $leftPadding The number of spaces to use for indentation * @return void * @see outputLine() diff --git a/Neos.Flow/Classes/Cli/CommandManager.php b/Neos.Flow/Classes/Cli/CommandManager.php index bf28383652..b05606b2c3 100644 --- a/Neos.Flow/Classes/Cli/CommandManager.php +++ b/Neos.Flow/Classes/Cli/CommandManager.php @@ -27,22 +27,22 @@ class CommandManager { /** - * @var array + * @var ?array */ protected $availableCommands = null; /** - * @var array + * @var ?array */ protected $shortCommandIdentifiers = null; /** - * @var Bootstrap + * @var ?Bootstrap */ protected $bootstrap; /** - * @var ObjectManagerInterface + * @var ?ObjectManagerInterface */ protected $objectManager; @@ -74,6 +74,9 @@ public function getAvailableCommands(): array { if ($this->availableCommands === null) { $this->availableCommands = []; + if ($this->objectManager === null) { + throw new \RuntimeException('missing object manager', 1744472974); + } foreach (static::getCommandControllerMethodArguments($this->objectManager) as $className => $methods) { foreach (array_keys($methods) as $methodName) { @@ -161,14 +164,13 @@ public function getShortestIdentifierForCommand(Command $command): string /** * Returns an array that contains all available command identifiers and their shortest non-ambiguous alias * - * @return array in the format array('full.command:identifier1' => 'alias1', 'full.command:identifier2' => 'alias2') + * @return array in the format array('full.command:identifier1' => 'alias1', 'full.command:identifier2' => 'alias2') */ protected function getShortCommandIdentifiers(): array { if ($this->shortCommandIdentifiers === null) { $this->shortCommandIdentifiers = []; $commandsByCommandName = []; - /** @var Command $availableCommand */ foreach ($this->getAvailableCommands() as $availableCommand) { list($packageKey, $controllerName, $commandName) = explode(':', $availableCommand->getCommandIdentifier()); if (!isset($commandsByCommandName[$commandName])) { @@ -181,6 +183,9 @@ protected function getShortCommandIdentifiers(): array } foreach ($this->getAvailableCommands() as $availableCommand) { list($packageKey, $controllerName, $commandName) = explode(':', $availableCommand->getCommandIdentifier()); + if ($this->bootstrap === null) { + throw new \RuntimeException('missing bootstrap', 1744472927); + } if (count($commandsByCommandName[$commandName][$controllerName]) > 1 || $this->bootstrap->isCompiletimeCommand($availableCommand->getCommandIdentifier())) { $packageKeyParts = array_reverse(explode('.', $packageKey)); for ($i = 1; $i <= count($packageKeyParts); $i++) { @@ -245,18 +250,21 @@ protected function commandMatchesIdentifier(Command $command, string $commandIde * * @param string $controllerObjectName * @param string $commandMethodName - * @return array + * @return array */ public function getCommandMethodParameters(string $controllerObjectName, string $commandMethodName): array { + if ($this->objectManager === null) { + throw new \RuntimeException('missing object Manager', 1744472898); + } $commandControllerMethodArgumentMap = static::getCommandControllerMethodArguments($this->objectManager); - return isset($commandControllerMethodArgumentMap[$controllerObjectName][$commandMethodName]) ? $commandControllerMethodArgumentMap[$controllerObjectName][$commandMethodName] : []; + return $commandControllerMethodArgumentMap[$controllerObjectName][$commandMethodName] ?? []; } /** * @param ObjectManagerInterface $objectManager - * @return array Array of method arguments per controller and method. + * @return array,array>> of method arguments per controller and method. * @Flow\CompileStatic */ public static function getCommandControllerMethodArguments(ObjectManagerInterface $objectManager): array @@ -269,12 +277,13 @@ public static function getCommandControllerMethodArguments(ObjectManagerInterfac if (!class_exists($className) || $reflectionService->isClassAbstract($className)) { continue; } - /** @var string $controllerObjectName */ - $controllerObjectName = $objectManager->getObjectNameByClassName($className); - $commandControllerMethodArgumentMap[$controllerObjectName] = []; + /** @todo what was this necessary for? + * $controllerObjectName = $objectManager->getObjectNameByClassName($className); + */ + $commandControllerMethodArgumentMap[$className] = []; foreach (get_class_methods($className) as $methodName) { if (str_ends_with($methodName, 'Command')) { - $commandControllerMethodArgumentMap[$className][$methodName] = $reflectionService->getMethodParameters($controllerObjectName, $methodName); + $commandControllerMethodArgumentMap[$className][$methodName] = $reflectionService->getMethodParameters($className, $methodName); } } } diff --git a/Neos.Flow/Classes/Cli/ConsoleOutput.php b/Neos.Flow/Classes/Cli/ConsoleOutput.php index 9f3c792a07..a7c4ed341e 100644 --- a/Neos.Flow/Classes/Cli/ConsoleOutput.php +++ b/Neos.Flow/Classes/Cli/ConsoleOutput.php @@ -89,7 +89,7 @@ public function getMaximumLineLength(): int * @see http://www.php.net/sprintf * * @param string $text Text to output - * @param array $arguments Optional arguments to use for sprintf + * @param array $arguments Optional arguments to use for sprintf * @return void */ public function output(string $text, array $arguments = []): void @@ -104,7 +104,7 @@ public function output(string $text, array $arguments = []): void * Outputs specified text to the console window and appends a line break * * @param string $text Text to output - * @param array $arguments Optional arguments to use for sprintf + * @param array $arguments Optional arguments to use for sprintf * @return void * @see output() * @see outputLines() @@ -119,7 +119,7 @@ public function outputLine(string $text = '', array $arguments = []): void * console window * * @param string $text Text to output - * @param array $arguments Optional arguments to use for sprintf + * @param array $arguments Optional arguments to use for sprintf * @param integer $leftPadding The number of spaces to use for indentation * @return void * @see outputLine() @@ -136,9 +136,9 @@ public function outputFormatted(string $text = '', array $arguments = [], int $l /** * Renders a table like output of the given $rows * - * @param array $rows - * @param array $headers - * @param string $headerTitle + * @param array $rows + * @param ?array $headers + * @param ?string $headerTitle */ public function outputTable(array $rows, ?array $headers = null, ?string $headerTitle = null): void { @@ -156,12 +156,12 @@ public function outputTable(array $rows, ?array $headers = null, ?string $header /** * Asks the user to select a value * - * @param string|array $question The question to ask. If an array each array item is turned into one line of a multi-line question - * @param array $choices List of choices to pick from + * @param string|array $question The question to ask. If an array each array item is turned into one line of a multi-line question + * @param array $choices List of choices to pick from * @param string|bool|int|float|null $default The default answer if the user enters nothing * @param boolean $multiSelect If true the result will be an array with the selected options. Multiple options can be given separated by commas * @param integer|null $attempts Max number of times to ask before giving up (null by default, which means infinite) - * @return integer|string|array Either the value for indexed arrays, the key for associative arrays or an array for multiple selections + * @return integer|string|array Either the value for indexed arrays, the key for associative arrays or an array for multiple selections * @throws \InvalidArgumentException */ public function select($question, array $choices, $default = null, bool $multiSelect = false, ?int $attempts = null) @@ -178,8 +178,8 @@ public function select($question, array $choices, $default = null, bool $multiSe /** * Asks a question to the user * - * @param string|array $question The question to ask. If an array each array item is turned into one line of a multi-line question - * @param string $default The default answer if none is given by the user + * @param string|array $question The question to ask. If an array each array item is turned into one line of a multi-line question + * @param ?string $default The default answer if none is given by the user * @return mixed The user answer * @throws \RuntimeException If there is no data to read in the input stream */ @@ -207,7 +207,7 @@ public function askQuestion(Question $question) * * The question will be asked until the user answers by nothing, yes, or no. * - * @param string|array $question The question to ask. If an array each array item is turned into one line of a multi-line question + * @param string|array $question The question to ask. If an array each array item is turned into one line of a multi-line question * @param boolean $default The default answer if the user enters nothing * @return boolean true if the user has confirmed, false otherwise */ @@ -221,7 +221,7 @@ public function askConfirmation($question, bool $default = true): bool /** * Asks a question to the user, the response is hidden * - * @param string|array $question The question. If an array each array item is turned into one line of a multi-line question + * @param string|array $question The question. If an array each array item is turned into one line of a multi-line question * @param Boolean $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not * @return mixed The answer * @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden @@ -244,7 +244,7 @@ public function askHiddenResponse($question, bool $fallback = true) * otherwise. * * @see https://symfony.com/doc/current/components/console/helpers/questionhelper.html#validating-the-answer - * @param string|array $question The question to ask. If an array each array item is turned into one line of a multi-line question + * @param string|array $question The question to ask. If an array each array item is turned into one line of a multi-line question * @param callable $validator A PHP callback that gets a value and is expected to return the (transformed) value or throw an exception if it wasn't valid * @param integer|null $attempts Max number of times to ask before giving up (null by default, which means infinite) * @param string $default The default answer if none is given by the user @@ -268,7 +268,7 @@ public function askAndValidate($question, callable $validator, ?int $attempts = * validated data when the data is valid and throw an exception * otherwise. * - * @param string|array $question The question to ask. If an array each array item is turned into one line of a multi-line question + * @param string|array $question The question to ask. If an array each array item is turned into one line of a multi-line question * @param callable $validator A PHP callback that gets a value and is expected to return the (transformed) value or throw an exception if it wasn't valid * @param integer|null $attempts Max number of times to ask before giving up (null by default, which means infinite) * @param boolean $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not @@ -384,7 +384,7 @@ public function getQuestionHelper(): QuestionHelper /** * If question is an array, split it into multi-line string * - * @param string|array $question + * @param string|array $question * @return string */ protected function combineQuestion($question): string @@ -405,12 +405,8 @@ public function getProgressBar(): ProgressBar { if ($this->progressBar === null) { $this->progressBar = new ProgressBar($this->output); - if (is_callable([$this->progressBar, 'minSecondsBetweenRedraws'])) { - $this->progressBar->minSecondsBetweenRedraws(0); - } - if (is_callable([$this->progressBar, 'maxSecondsBetweenRedraws'])) { - $this->progressBar->maxSecondsBetweenRedraws(0); - } + $this->progressBar->minSecondsBetweenRedraws(0); + $this->progressBar->maxSecondsBetweenRedraws(0); } return $this->progressBar; } diff --git a/Neos.Flow/Classes/Cli/Dispatcher.php b/Neos.Flow/Classes/Cli/Dispatcher.php index 9b123c6b31..90d219aa01 100644 --- a/Neos.Flow/Classes/Cli/Dispatcher.php +++ b/Neos.Flow/Classes/Cli/Dispatcher.php @@ -27,7 +27,7 @@ class Dispatcher /** * @param \Neos\Flow\SignalSlot\Dispatcher $signalDispatcher */ - public function injectSignalDispatcher(\Neos\Flow\SignalSlot\Dispatcher $signalDispatcher) + public function injectSignalDispatcher(\Neos\Flow\SignalSlot\Dispatcher $signalDispatcher): void { $this->signalDispatcher = $signalDispatcher; } @@ -35,7 +35,7 @@ public function injectSignalDispatcher(\Neos\Flow\SignalSlot\Dispatcher $signalD /** * @param ObjectManagerInterface $objectManager */ - public function injectObjectManager(ObjectManagerInterface $objectManager) + public function injectObjectManager(ObjectManagerInterface $objectManager): void { $this->objectManager = $objectManager; } @@ -108,16 +108,11 @@ protected function emitAfterControllerInvocation(Request $request, Response $res protected function resolveController(Request $request): CommandControllerInterface { $controllerObjectName = $request->getControllerObjectName(); - if ($controllerObjectName === '') { + if ($controllerObjectName === null) { $exceptionMessage = 'No controller could be resolved which would match your request'; throw new InvalidCommandControllerException($exceptionMessage, 1565878092); } - $controller = $this->objectManager->get($controllerObjectName); - if (!$controller instanceof CommandControllerInterface) { - throw new InvalidCommandControllerException('Invalid controller "' . $request->getControllerObjectName() . '". The controller must be a valid request handling controller, ' . (is_object($controller) ? get_class($controller) : gettype($controller)) . ' given.', 1565878098); - } - - return $controller; + return $this->objectManager->get($controllerObjectName); } } diff --git a/Neos.Flow/Classes/Cli/Request.php b/Neos.Flow/Classes/Cli/Request.php index dcbfb5e4af..e9c26e5b52 100644 --- a/Neos.Flow/Classes/Cli/Request.php +++ b/Neos.Flow/Classes/Cli/Request.php @@ -22,7 +22,7 @@ class Request { /** - * @var string + * @var ?class-string */ protected $controllerObjectName; @@ -38,12 +38,12 @@ class Request /** * The arguments for this request - * @var array + * @var array */ protected $arguments = []; /** - * @var array + * @var array */ protected $exceedingArguments = []; @@ -81,7 +81,7 @@ public function isDispatched(): bool /** * Sets the object name of the controller * - * @param string $controllerObjectName Object name of the controller which processes this request + * @param class-string $controllerObjectName Object name of the controller which processes this request * @return void */ public function setControllerObjectName(string $controllerObjectName) @@ -93,9 +93,9 @@ public function setControllerObjectName(string $controllerObjectName) /** * Returns the object name of the controller * - * @return string The controller's object name + * @return ?class-string The controller's object name */ - public function getControllerObjectName(): string + public function getControllerObjectName(): ?string { return $this->controllerObjectName; } @@ -132,6 +132,9 @@ public function getControllerCommandName(): string public function getCommand(): Command { if ($this->command === null) { + if ($this->controllerObjectName === null) { + throw new \RuntimeException('missing controller object name', 1744472740); + } $this->command = new Command($this->controllerObjectName, $this->controllerCommandName); } return $this->command; @@ -156,7 +159,7 @@ public function setArgument(string $argumentName, $value): void /** * Sets the whole arguments array and therefore replaces any arguments which existed before. * - * @param array $arguments An array of argument names and their values + * @param array $arguments An array of argument names and their values * @return void */ public function setArguments(array $arguments) @@ -193,7 +196,7 @@ public function hasArgument(string $argumentName): bool /** * Returns an ArrayObject of arguments and their values * - * @return array Array of arguments and their values (which may be arguments and values as well) + * @return array Array of arguments and their values (which may be arguments and values as well) */ public function getArguments(): array { @@ -203,7 +206,7 @@ public function getArguments(): array /** * Sets the exceeding arguments * - * @param array $exceedingArguments Numeric array of exceeding arguments + * @param array $exceedingArguments Numeric array of exceeding arguments * @return void */ public function setExceedingArguments(array $exceedingArguments) @@ -219,7 +222,7 @@ public function setExceedingArguments(array $exceedingArguments) * ./flow acme:foo --argument1 Foo --argument2 Bar baz quux * this method would return array(0 => 'baz', 1 => 'quux') * - * @return array Numeric array of exceeding argument values + * @return array Numeric array of exceeding argument values */ public function getExceedingArguments(): array { diff --git a/Neos.Flow/Classes/Cli/RequestBuilder.php b/Neos.Flow/Classes/Cli/RequestBuilder.php index df1ee24411..cdc4749817 100644 --- a/Neos.Flow/Classes/Cli/RequestBuilder.php +++ b/Neos.Flow/Classes/Cli/RequestBuilder.php @@ -163,13 +163,14 @@ public function build($commandLine): Request return $request; } - /** @var string $controllerObjectName */ - $controllerObjectName = $this->objectManager->getObjectNameByClassName($command->getControllerClassName()); + /** @todo what was this necessary for? + *$controllerObjectName = $this->objectManager->getObjectNameByClassName($command->getControllerClassName()); + */ $controllerCommandName = $command->getControllerCommandName(); - $request->setControllerObjectName($controllerObjectName); + $request->setControllerObjectName($command->getControllerClassName()); $request->setControllerCommandName($controllerCommandName); - list($commandLineArguments, $exceedingCommandLineArguments) = $this->parseRawCommandLineArguments($rawCommandLineArguments, $controllerObjectName, $controllerCommandName); + list($commandLineArguments, $exceedingCommandLineArguments) = $this->parseRawCommandLineArguments($rawCommandLineArguments, $command->getControllerClassName(), $controllerCommandName); $request->setArguments($commandLineArguments); $request->setExceedingArguments($exceedingCommandLineArguments); @@ -180,10 +181,10 @@ public function build($commandLine): Request * Takes an array of unparsed command line arguments and options and converts it separated * by named arguments, options and unnamed arguments. * - * @param array $rawCommandLineArguments The unparsed command parts (such as "--foo") as an array + * @param array $rawCommandLineArguments The unparsed command parts (such as "--foo") as an array * @param string $controllerObjectName Object name of the designated command controller * @param string $controllerCommandName Command name of the recognized command (ie. method name without "Command" suffix) - * @return array All and exceeding command line arguments + * @return array{0: array, 1: array} All and exceeding command line arguments * @throws InvalidArgumentMixingException */ protected function parseRawCommandLineArguments(array $rawCommandLineArguments, string $controllerObjectName, string $controllerCommandName): array @@ -281,7 +282,7 @@ protected function extractArgumentNameFromCommandLinePart(string $commandLinePar * Returns the value of the first argument of the given input array. Shifts the parsed argument off the array. * * @param string $currentArgument The current argument - * @param array &$rawCommandLineArguments Array of the remaining command line arguments + * @param array &$rawCommandLineArguments Array of the remaining command line arguments * @param string $expectedArgumentType The expected type of the current argument, because booleans get special attention * @return mixed The value of the first argument */ diff --git a/Neos.Flow/Classes/Cli/Response.php b/Neos.Flow/Classes/Cli/Response.php index 196653d923..042a1827bd 100644 --- a/Neos.Flow/Classes/Cli/Response.php +++ b/Neos.Flow/Classes/Cli/Response.php @@ -61,7 +61,6 @@ class Response * * @param integer $exitCode * @return void - * @throws \InvalidArgumentException * @api */ public function setExitCode(int $exitCode): void @@ -190,21 +189,21 @@ public function send(): void $content = $this->getContent(); if ($this->hasColorSupport() === true) { - $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_BRIGHT . "m\$1\x1B[0m", $content); - $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_ITALIC . "m\$1\x1B[0m", $content); - $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_UNDERLINED . "m\$1\x1B[0m", $content); - $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_INVERSE . "m\$1\x1B[0m", $content); - $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_STRIKETHROUGH . "m\$1\x1B[0m", $content); - $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_ERROR . "m\$1\x1B[0m", $content); - $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_SUCCESS . "m\$1\x1B[0m", $content); + $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_BRIGHT . "m\$1\x1B[0m", $content) ?: ''; + $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_ITALIC . "m\$1\x1B[0m", $content) ?: ''; + $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_UNDERLINED . "m\$1\x1B[0m", $content) ?: ''; + $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_INVERSE . "m\$1\x1B[0m", $content) ?: ''; + $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_STRIKETHROUGH . "m\$1\x1B[0m", $content) ?: ''; + $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_ERROR . "m\$1\x1B[0m", $content) ?: ''; + $content = preg_replace('|\(((?!\).)*)\|', "\x1B[" . self::STYLE_SUCCESS . "m\$1\x1B[0m", $content) ?: ''; } else { - $content = preg_replace('|\(((?!\).)*)\|', "$1", $content); - $content = preg_replace('|\(((?!\).)*)\|', "$1", $content); - $content = preg_replace('|\(((?!\).)*)\|', "$1", $content); - $content = preg_replace('|\(((?!\).)*)\|', "$1", $content); - $content = preg_replace('|\(((?!\).)*)\|', "$1", $content); - $content = preg_replace('|\(((?!\).)*)\|', "$1", $content); - $content = preg_replace('|\(((?!\).)*)\|', "$1", $content); + $content = preg_replace('|\(((?!\).)*)\|', "$1", $content) ?: ''; + $content = preg_replace('|\(((?!\).)*)\|', "$1", $content) ?: ''; + $content = preg_replace('|\(((?!\).)*)\|', "$1", $content) ?: ''; + $content = preg_replace('|\(((?!\).)*)\|', "$1", $content) ?: ''; + $content = preg_replace('|\(((?!\).)*)\|', "$1", $content) ?: ''; + $content = preg_replace('|\(((?!\).)*)\|', "$1", $content) ?: ''; + $content = preg_replace('|\(((?!\).)*)\|', "$1", $content) ?: ''; } echo $content; } diff --git a/Neos.Flow/Classes/Cli/SlaveRequestHandler.php b/Neos.Flow/Classes/Cli/SlaveRequestHandler.php index 794e96bf48..e8540bd22e 100644 --- a/Neos.Flow/Classes/Cli/SlaveRequestHandler.php +++ b/Neos.Flow/Classes/Cli/SlaveRequestHandler.php @@ -88,7 +88,7 @@ public function handleRequest() try { $response = null; while (true) { - $commandLine = trim(fgets(STDIN)); + $commandLine = trim(fgets(STDIN) ?: ''); $trimmedCommandLine = trim($commandLine); $logger->info(sprintf('Received command "%s".', $trimmedCommandLine), LogEnvironment::fromMethodName(__METHOD__)); if ($commandLine === "QUIT\n") { diff --git a/Neos.Flow/Classes/Command/CacheCommandController.php b/Neos.Flow/Classes/Command/CacheCommandController.php index 414e3724ec..ab9b263535 100644 --- a/Neos.Flow/Classes/Command/CacheCommandController.php +++ b/Neos.Flow/Classes/Command/CacheCommandController.php @@ -271,7 +271,6 @@ public function listCommand(bool $quiet = false) $this->outputLine('The following caches contain errors or warnings: %s', [implode(', ', $erroneousCaches)]); $quiet || $this->outputLine('Use the neos.flow:cache:show Command for more information'); $this->quit(1); - return; } } @@ -291,7 +290,6 @@ public function showCommand(string $cacheIdentifier) $this->outputLine('A Cache with id "%s" is not configured.', [$cacheIdentifier]); $this->outputLine('Use the neos.flow:cache:list command to get a list of all configured Caches.'); $this->quit(1); - return; } $cacheConfigurations = $this->cacheManager->getCacheConfigurations(); $defaultConfiguration = $cacheConfigurations['Default']; @@ -302,7 +300,7 @@ public function showCommand(string $cacheIdentifier) $this->outputLine('Backend: %s', [TypeHandling::getTypeForValue($cacheBackend)]); $options = $cacheConfiguration['backendOptions'] ?? $defaultConfiguration['backendOptions']; $this->outputLine('Backend Options:'); - $this->outputLine(json_encode($options, JSON_PRETTY_PRINT)); + $this->outputLine(json_encode($options, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); if ($cacheBackend instanceof WithStatusInterface) { $this->outputLine(); @@ -313,7 +311,6 @@ public function showCommand(string $cacheIdentifier) if ($cacheStatus->hasErrors() || $cacheStatus->hasWarnings()) { $this->quit(1); - return; } } } @@ -338,20 +335,17 @@ public function setupCommand(string $cacheIdentifier) $this->outputLine('A Cache with id "%s" is not configured.', [$cacheIdentifier]); $this->outputLine('Use the neos.flow:cache:list command to get a list of all configured Caches.'); $this->quit(1); - return; } $cacheBackend = $cache->getBackend(); if (!$cacheBackend instanceof WithSetupInterface) { $this->outputLine('The Cache "%s" is configured to use the backend "%s" but this does not implement the WithSetupInterface.', [$cacheIdentifier, TypeHandling::getTypeForValue($cacheBackend)]); $this->quit(1); - return; } $this->outputLine('Setting up backend %s for cache "%s"', [TypeHandling::getTypeForValue($cacheBackend), $cache->getIdentifier()]); $setupResult = $cacheBackend->setup(); $this->renderResult($setupResult); if ($setupResult->hasErrors() || $setupResult->hasWarnings()) { $this->quit(1); - return; } } @@ -390,7 +384,6 @@ public function setupAllCommand(bool $quiet = false) } if ($hasErrorsOrWarnings) { $this->quit(1); - return; } } diff --git a/Neos.Flow/Classes/Command/ConfigurationCommandController.php b/Neos.Flow/Classes/Command/ConfigurationCommandController.php index 742642ee16..1f0404da13 100644 --- a/Neos.Flow/Classes/Command/ConfigurationCommandController.php +++ b/Neos.Flow/Classes/Command/ConfigurationCommandController.php @@ -75,7 +75,7 @@ public function showCommand(string $type = 'Settings', string $path = '', int $d } $configuration = self::truncateArrayAtDepth($configuration, $depth); $typeAndPath = $type . ($path ? ': ' . $path : ''); - if ($configuration === null) { + if ($configuration === []) { $this->outputLine('Configuration "%s" was empty!', [$typeAndPath]); } else { $yaml = Yaml::dump($configuration, 99); @@ -96,8 +96,10 @@ public function showCommand(string $type = 'Settings', string $path = '', int $d } /** + * @param array $array * @param int $maximumDepth 0 for no truncation and 1 to only show the first keys of the array * @param int $currentLevel 1 for the start and will be incremented recursively + * @return array */ private static function truncateArrayAtDepth(array $array, int $maximumDepth, int $currentLevel = 1): array { @@ -167,7 +169,6 @@ public function validateCommand(?string $type = null, ?string $path = null, bool $this->outputLine('Exception:'); $this->outputFormatted($exception->getMessage(), [], 4); $this->quit(2); - return; } if ($verbose) { diff --git a/Neos.Flow/Classes/Command/CoreCommandController.php b/Neos.Flow/Classes/Command/CoreCommandController.php index 4e1607e0e8..60d8df9554 100644 --- a/Neos.Flow/Classes/Command/CoreCommandController.php +++ b/Neos.Flow/Classes/Command/CoreCommandController.php @@ -74,7 +74,7 @@ class CoreCommandController extends CommandController protected $aopProxyClassBuilder; /** - * @var ProxyClassBuilder + * @var ProxyClassBuilder */ protected $dependencyInjectionProxyClassBuilder; @@ -147,7 +147,7 @@ public function injectAopProxyClassBuilder(AopProxyClassBuilder $aopProxyClassBu } /** - * @param ProxyClassBuilder $dependencyInjectionProxyClassBuilder + * @param ProxyClassBuilder $dependencyInjectionProxyClassBuilder * @return void */ public function injectDependencyInjectionProxyClassBuilder(ProxyClassBuilder $dependencyInjectionProxyClassBuilder) diff --git a/Neos.Flow/Classes/Command/DatabaseCommandController.php b/Neos.Flow/Classes/Command/DatabaseCommandController.php index 2700942e32..2e06f10971 100644 --- a/Neos.Flow/Classes/Command/DatabaseCommandController.php +++ b/Neos.Flow/Classes/Command/DatabaseCommandController.php @@ -28,7 +28,7 @@ class DatabaseCommandController extends CommandController { /** * @Flow\InjectConfiguration(path="persistence") - * @var array + * @var array */ protected $persistenceSettings = []; @@ -86,7 +86,7 @@ protected function initializeConnection() * @throws DBALException * @throws StopActionException */ - public function setCharsetCommand(string $characterSet = 'utf8mb4', string $collation = 'utf8mb4_unicode_ci', ?string $output = null, bool $verbose = false) + public function setCharsetCommand(string $characterSet = 'utf8mb4', string $collation = 'utf8mb4_unicode_ci', ?string $output = null, bool $verbose = false): void { if (!in_array($this->persistenceSettings['backendOptions']['driver'], ['pdo_mysql', 'mysqli'])) { $this->outputLine('Database charset/collation fixing is only supported on MySQL.'); @@ -118,13 +118,13 @@ public function setCharsetCommand(string $characterSet = 'utf8mb4', string $coll * @throws ConnectionException * @throws DBALException */ - protected function convertToCharacterSetAndCollation(string $characterSet, string $collation, ?string $outputPathAndFilename = null, bool $verbose = false) + protected function convertToCharacterSetAndCollation(string $characterSet, string $collation, ?string $outputPathAndFilename = null, bool $verbose = false): void { $statements = ['SET foreign_key_checks = 0']; $statements[] = 'ALTER DATABASE ' . $this->connection->quoteIdentifier($this->persistenceSettings['backendOptions']['dbname']) . ' CHARACTER SET ' . $characterSet . ' COLLATE ' . $collation; - $tableNames = $this->connection->createSchemaManager()->listTableNames() ?? []; + $tableNames = $this->connection->createSchemaManager()->listTableNames(); foreach ($tableNames as $tableName) { $statements[] = 'ALTER TABLE ' . $this->connection->quoteIdentifier($tableName) . ' DEFAULT CHARACTER SET ' . $characterSet . ' COLLATE ' . $collation; $statements[] = 'ALTER TABLE ' . $this->connection->quoteIdentifier($tableName) . ' CONVERT TO CHARACTER SET ' . $characterSet . ' COLLATE ' . $collation; diff --git a/Neos.Flow/Classes/Command/DoctrineCommandController.php b/Neos.Flow/Classes/Command/DoctrineCommandController.php index 66963e7e53..45a7eb7e2b 100644 --- a/Neos.Flow/Classes/Command/DoctrineCommandController.php +++ b/Neos.Flow/Classes/Command/DoctrineCommandController.php @@ -39,7 +39,7 @@ class DoctrineCommandController extends CommandController { /** - * @var array + * @var array */ protected $settings = []; @@ -70,7 +70,7 @@ class DoctrineCommandController extends CommandController /** * Injects the Flow settings, only the persistence part is kept for further use * - * @param array $settings + * @param array $settings * @return void */ public function injectSettings(array $settings): void @@ -510,9 +510,8 @@ public function migrationGenerateCommand(bool $diffAgainstCurrent = true, ?strin $migrationPlatformFolderPart = $migrationFolder ?? $this->doctrineService->getMigrationFolderName(); - if ($selectedPackage !== $choices[0]) { + if (is_string($selectedPackage) && $selectedPackage !== $choices[0]) { $selectedPackage = $packages[$selectedPackage]; - /** @var Package $selectedPackage */ $targetPathAndFilename = Files::concatenatePaths([$selectedPackage->getPackagePath(), 'Migrations', $migrationPlatformFolderPart, basename($migrationClassPathAndFilename)]); Files::createDirectoryRecursively(dirname($targetPathAndFilename)); rename($migrationClassPathAndFilename, $targetPathAndFilename); @@ -569,7 +568,7 @@ private function normalizeVersion(string $version): string return sprintf('Neos\Flow\Persistence\Doctrine\Migrations\Version%s', $version); } - private function maybeOutputMigrationFolderWarning(?string $migrationFolder = null) + private function maybeOutputMigrationFolderWarning(?string $migrationFolder = null): void { if ($migrationFolder !== null) { $this->outputLine('Migration folder override is in effect, migration files are not searched in folder matching the configured connection but in ...Migrations/%s/', [$migrationFolder]); diff --git a/Neos.Flow/Classes/Command/HelpCommandController.php b/Neos.Flow/Classes/Command/HelpCommandController.php index 3f44acb8c5..bcf34ee1c5 100644 --- a/Neos.Flow/Classes/Command/HelpCommandController.php +++ b/Neos.Flow/Classes/Command/HelpCommandController.php @@ -15,6 +15,7 @@ use Neos\Flow\Cli\Command; use Neos\Flow\Cli\CommandArgumentDefinition; use Neos\Flow\Cli\CommandController; +use Neos\Flow\Cli\CommandControllerInterface; use Neos\Flow\Core\Bootstrap; use Neos\Flow\Mvc\Exception\AmbiguousCommandIdentifierException; use Neos\Flow\Mvc\Exception\CommandException; @@ -176,7 +177,7 @@ protected function displayHelpForCommand(Command $command) if (!$commandArgumentDefinition->isRequired()) { $hasOptions = true; } else { - $usage .= sprintf(' <%s>', strtolower(preg_replace('/([A-Z])/', ' $1', $commandArgumentDefinition->getName()))); + $usage .= sprintf(' <%s>', strtolower(preg_replace('/([A-Z])/', ' $1', $commandArgumentDefinition->getName()) ?: '')); } } @@ -270,18 +271,17 @@ public function errorCommand(CommandException $exception) * added to the commands array of this class. * * @param array $commands - * @return array in the format array('' => array('', array('' => $command1, '' => $command2))) + * @return array,array>> in the format array('' => array('', array('' => $command1, '' => $command2))) */ protected function buildCommandsIndex(array $commands) { $commandsByPackagesAndControllers = []; - /** @var Command $command */ foreach ($commands as $command) { if ($command->isInternal()) { continue; } $commandIdentifier = $command->getCommandIdentifier(); - $packageKey = strstr($commandIdentifier, ':', true); + $packageKey = (string)strstr($commandIdentifier, ':', true); $commandControllerClassName = $command->getControllerClassName(); $commandName = $command->getControllerCommandName(); $commandsByPackagesAndControllers[$packageKey][$commandControllerClassName][$commandName] = $command; diff --git a/Neos.Flow/Classes/Command/MiddlewareCommandController.php b/Neos.Flow/Classes/Command/MiddlewareCommandController.php index c05b0080da..cb2279355d 100644 --- a/Neos.Flow/Classes/Command/MiddlewareCommandController.php +++ b/Neos.Flow/Classes/Command/MiddlewareCommandController.php @@ -22,7 +22,7 @@ final class MiddlewareCommandController extends CommandController { /** * @Flow\InjectConfiguration(path="http.middlewares") - * @var array + * @var array */ protected $chainConfiguration; diff --git a/Neos.Flow/Classes/Command/PackageCommandController.php b/Neos.Flow/Classes/Command/PackageCommandController.php index f29fd399f3..b2ca710ceb 100644 --- a/Neos.Flow/Classes/Command/PackageCommandController.php +++ b/Neos.Flow/Classes/Command/PackageCommandController.php @@ -15,7 +15,6 @@ use Neos\Flow\Cli\CommandController; use Neos\Flow\Composer\ComposerUtility; use Neos\Flow\Package\PackageInterface; -use Neos\Flow\Package\PackageKeyAwareInterface; use Neos\Flow\Package\PackageManager; /** @@ -97,7 +96,6 @@ public function listCommand(bool $loadingOrder = false) } $this->outputLine('PACKAGES:'); - /** @var PackageInterface|PackageKeyAwareInterface $package */ foreach ($availablePackages as $package) { $this->outputLine(' ' . str_pad($package->getPackageKey(), $longestPackageKey + 3) . str_pad($package->getInstalledVersion(), 15)); } @@ -106,7 +104,7 @@ public function listCommand(bool $loadingOrder = false) /** * Rescan package availability and recreates the PackageStates configuration. */ - public function rescanCommand() + public function rescanCommand(): void { $packageStates = $this->packageManager->rescanPackages(); diff --git a/Neos.Flow/Classes/Command/ResourceCommandController.php b/Neos.Flow/Classes/Command/ResourceCommandController.php index 60075d7cfb..c3d9b4a5a6 100644 --- a/Neos.Flow/Classes/Command/ResourceCommandController.php +++ b/Neos.Flow/Classes/Command/ResourceCommandController.php @@ -249,13 +249,9 @@ public function cleanCommand() foreach ($brokenResources as $resource) { if ($mediaPackagePresent) { $assets = $assetRepository->findByResource($resource); - if ($assets !== null) { - $relatedAssets[$resource] = $assets; - } + $relatedAssets[$resource] = $assets; $thumbnails = $thumbnailRepository->findByResource($resource); - if ($assets !== null) { - $relatedThumbnails[$resource] = $thumbnails; - } + $relatedThumbnails[$resource] = $thumbnails; } } } @@ -306,6 +302,7 @@ public function cleanCommand() $this->persistenceManager->persistAll(); } $brokenResourcesCounter = count($brokenResources); + /** @phpstan-ignore greater.alwaysTrue (not sure about this) */ if ($brokenResourcesCounter > 0) { $this->outputLine('Removed %s resource object(s) from the database.', [$brokenResourcesCounter]); } @@ -319,7 +316,6 @@ public function cleanCommand() case 'n': $this->outputLine('Did not delete any resource objects.'); $this->quit(0); - break; } } } diff --git a/Neos.Flow/Classes/Command/RoutingCommandController.php b/Neos.Flow/Classes/Command/RoutingCommandController.php index 18d932d542..32678b5749 100644 --- a/Neos.Flow/Classes/Command/RoutingCommandController.php +++ b/Neos.Flow/Classes/Command/RoutingCommandController.php @@ -23,7 +23,6 @@ use Neos\Flow\Mvc\Routing\Dto\RouteContext; use Neos\Flow\Mvc\Routing\Dto\RouteParameters; 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\RoutesProviderInterface; use Neos\Flow\ObjectManagement\ObjectManagerInterface; @@ -207,9 +206,8 @@ public function resolveCommand(string $package, ?string $controller = null, ?str $this->quit(1); } - /** @var UriConstraints $uriConstraints */ $uriConstraints = $resolvedRoute->getResolvedUriConstraints(); - $resolvedUri = $uriConstraints->applyTo($resolveContext->getBaseUri(), $resolveContext->isForceAbsoluteUri()); + $resolvedUri = $uriConstraints?->applyTo($resolveContext->getBaseUri(), $resolveContext->isForceAbsoluteUri()) ?: $resolveContext->getBaseUri(); $this->outputLine('Route resolved!'); $this->outputLine('Name: %s', [$resolvedRoute->getName()]); @@ -299,7 +297,7 @@ public function matchCommand(string $uri, ?string $method = null, ?string $param $this->outputLine(); $this->outputLine('Results:'); - $matchResults = $matchedRoute->getMatchResults(); + $matchResults = $matchedRoute->getMatchResults() ?: []; $this->outputArray($matchResults, 2); $this->outputLine(); @@ -338,7 +336,7 @@ private function createRouteParametersFromJson(?string $json): RouteParameters * Parses the given JSON string as array * * @param string|null $json - * @return array + * @return array * @throws StopCommandException */ private function parseJsonToArray(?string $json): array @@ -361,7 +359,7 @@ private function parseJsonToArray(?string $json): array /** * Outputs a (potentially multi-dimensional) array to the console * - * @param array $array + * @param array $array * @param int $indention */ private function outputArray(array $array, int $indention): void @@ -391,7 +389,11 @@ private function outputArray(array $array, int $indention): void */ private function outputControllerObjectName(string $package, ?string $subpackage, ?string $controller): void { - $possibleControllerObjectName = str_replace(['@package', '@subpackage', '@controller', '\\\\'], [str_replace('.', '\\', $package), $subpackage, $controller, '\\'], '@package\@subpackage\Controller\@controllerController'); + $possibleControllerObjectName = str_replace( + ['@package', '@subpackage', '@controller', '\\\\'], + array_filter([str_replace('.', '\\', $package), $subpackage, $controller, '\\']), + '@package\@subpackage\Controller\@controllerController' + ); $controllerObjectName = $this->objectManager->getCaseSensitiveObjectName($possibleControllerObjectName); if ($controllerObjectName === null) { $this->outputLine('%s (no corresponding class exists)', [$possibleControllerObjectName]); diff --git a/Neos.Flow/Classes/Command/SecurityCommandController.php b/Neos.Flow/Classes/Command/SecurityCommandController.php index c33dc1f8d2..c677eeb476 100644 --- a/Neos.Flow/Classes/Command/SecurityCommandController.php +++ b/Neos.Flow/Classes/Command/SecurityCommandController.php @@ -89,6 +89,9 @@ public function importPublicKeyCommand() $keyData = ''; // no file_get_contents here because it does not work on php://stdin $fp = fopen('php://stdin', 'rb'); + if ($fp === false) { + throw new \RuntimeException('Unable to open php://stdin', 1744497200); + } while (!feof($fp)) { $keyData .= fgets($fp, 4096); } @@ -144,6 +147,9 @@ public function importPrivateKeyCommand(bool $usedForPasswords = false) $keyData = ''; // no file_get_contents here because it does not work on php://stdin $fp = fopen('php://stdin', 'rb'); + if ($fp === false) { + throw new \RuntimeException('Unable to open php://stdin', 1744497170); + } while (!feof($fp)) { $keyData .= fgets($fp, 4096); } @@ -160,7 +166,7 @@ public function importPrivateKeyCommand(bool $usedForPasswords = false) * @param string $privilegeType The privilege type ("entity", "method" or the FQN of a class implementing PrivilegeInterface) * @param string $roles A comma separated list of role identifiers. Shows policy for an unauthenticated user when left empty. */ - public function showEffectivePolicyCommand(string $privilegeType, string $roles = '') + public function showEffectivePolicyCommand(string $privilegeType, string $roles = ''): void { $systemRoleIdentifiers = ['Neos.Flow:Everybody', 'Neos.Flow:Anonymous', 'Neos.Flow:AuthenticatedUser']; @@ -323,11 +329,7 @@ public function showMethodsForPrivilegeTargetCommand(string $privilegeTarget) $matchedClassesAndMethods = []; foreach ($this->reflectionService->getAllClassNames() as $className) { - try { - $reflectionClass = new \ReflectionClass($className); - } catch (\ReflectionException $exception) { - continue; - } + $reflectionClass = new \ReflectionClass($className); foreach ($reflectionClass->getMethods() as $reflectionMethod) { $methodName = $reflectionMethod->getName(); if ($privilege->matchesMethod($className, $methodName)) { diff --git a/Neos.Flow/Classes/Command/ServerCommandController.php b/Neos.Flow/Classes/Command/ServerCommandController.php index 8effb1c4b6..775dec137a 100644 --- a/Neos.Flow/Classes/Command/ServerCommandController.php +++ b/Neos.Flow/Classes/Command/ServerCommandController.php @@ -24,7 +24,14 @@ class ServerCommandController extends CommandController { /** * @Flow\InjectConfiguration - * @var array + * @var array{ + * core: array{ + * context: string, + * phpBinaryPathAndFilename: string, + * subRequestPhpIniPathAndFilename?: mixed, + * subRequestEnvironmentVariables?: array, + * }, + * } */ protected $settings; diff --git a/Neos.Flow/Classes/Composer/ComposerUtility.php b/Neos.Flow/Classes/Composer/ComposerUtility.php index 499cda76af..206db2954e 100644 --- a/Neos.Flow/Classes/Composer/ComposerUtility.php +++ b/Neos.Flow/Classes/Composer/ComposerUtility.php @@ -26,14 +26,14 @@ class ComposerUtility /** * Runtime cache for composer.json data * - * @var array + * @var array> */ protected static $composerManifestCache; /** * Runtime cache for composer.lock data * - * @var array + * @var array */ protected static $composerLockCache; @@ -41,15 +41,12 @@ class ComposerUtility * Returns contents of Composer manifest - or part there of. * * @param string $manifestPath - * @param string $configurationPath Optional. Only return the part of the manifest indexed by configurationPath - * @return array|mixed + * @param ?string $configurationPath Optional. Only return the part of the manifest indexed by configurationPath + * @return mixed */ public static function getComposerManifest(string $manifestPath, ?string $configurationPath = null) { $composerManifest = static::readComposerManifest($manifestPath); - if ($composerManifest === null) { - return null; - } if ($configurationPath !== null) { return ObjectAccess::getPropertyPath($composerManifest, $configurationPath); @@ -60,7 +57,7 @@ public static function getComposerManifest(string $manifestPath, ?string $config /** * Read the content of the composer.lock * - * @return array + * @return array */ public static function readComposerLock(): array { @@ -72,8 +69,8 @@ public static function readComposerLock(): array return []; } - $json = file_get_contents(FLOW_PATH_ROOT . 'composer.lock'); - $composerLock = json_decode($json, true); + $json = file_get_contents(FLOW_PATH_ROOT . 'composer.lock') ?: ''; + $composerLock = json_decode($json, true, flags: JSON_THROW_ON_ERROR); $composerPackageVersions = isset($composerLock['packages']) ? $composerLock['packages'] : []; $composerPackageDevVersions = isset($composerLock['packages-dev']) ? $composerLock['packages-dev'] : []; self::$composerLockCache = array_merge($composerPackageVersions, $composerPackageDevVersions); @@ -85,7 +82,7 @@ public static function readComposerLock(): array * Read the content of composer.json in the given path * * @param string $manifestPath - * @return array + * @return array * @throws Exception\MissingPackageManifestException */ protected static function readComposerManifest(string $manifestPath): array @@ -98,8 +95,8 @@ protected static function readComposerManifest(string $manifestPath): array if (!is_file($manifestPathAndFilename)) { throw new Exception\MissingPackageManifestException(sprintf('No composer manifest file found at "%s".', $manifestPathAndFilename), 1349868540); } - $json = file_get_contents($manifestPathAndFilename); - $composerManifest = json_decode($json, true); + $json = file_get_contents($manifestPathAndFilename) ?: ''; + $composerManifest = json_decode($json, true, flags: JSON_THROW_ON_ERROR); if ($composerManifest === null) { throw new Exception\InvalidPackageManifestException(sprintf('The composer manifest file found at "%s" could not be parsed. Check for JSON syntax errors!', $manifestPathAndFilename), 1493909988); @@ -131,8 +128,8 @@ public static function isFlowPackageType(string $packageType): bool * * @param string $manifestPath * @param FlowPackageKey $packageKey - * @param array $composerManifestData - * @return array the manifest data written + * @param array $composerManifestData + * @return array the manifest data written */ public static function writeComposerManifest(string $manifestPath, FlowPackageKey $packageKey, array $composerManifestData = []): array { @@ -140,9 +137,7 @@ public static function writeComposerManifest(string $manifestPath, FlowPackageKe 'description' => '' ]; - if ($composerManifestData !== null) { - $manifest = array_merge($manifest, $composerManifestData); - } + $manifest = array_merge($manifest, $composerManifestData); if (!isset($manifest['name']) || empty($manifest['name'])) { $manifest['name'] = $packageKey->deriveComposerPackageName(); } diff --git a/Neos.Flow/Classes/Composer/InstallerScripts.php b/Neos.Flow/Classes/Composer/InstallerScripts.php index b39e588429..ca0325e324 100644 --- a/Neos.Flow/Classes/Composer/InstallerScripts.php +++ b/Neos.Flow/Classes/Composer/InstallerScripts.php @@ -37,19 +37,23 @@ class InstallerScripts */ public static function postUpdateAndInstall(Event $event): void { + $workingDirectory = getcwd(); + if ($workingDirectory === false) { + throw new \RuntimeException('Could not resolve working directory', 1744470280); + } if (!defined('FLOW_PATH_ROOT')) { - define('FLOW_PATH_ROOT', Files::getUnixStylePath(getcwd()) . '/'); + define('FLOW_PATH_ROOT', Files::getUnixStylePath($workingDirectory) . '/'); } if (!defined('FLOW_PATH_PACKAGES')) { - define('FLOW_PATH_PACKAGES', Files::getUnixStylePath(getcwd()) . '/Packages/'); + define('FLOW_PATH_PACKAGES', Files::getUnixStylePath($workingDirectory) . '/Packages/'); } if (!defined('FLOW_PATH_CONFIGURATION')) { - define('FLOW_PATH_CONFIGURATION', Files::getUnixStylePath(getcwd()) . '/Configuration/'); + define('FLOW_PATH_CONFIGURATION', Files::getUnixStylePath($workingDirectory) . '/Configuration/'); } if (!defined('FLOW_PATH_TEMPORARY_BASE')) { - define('FLOW_PATH_TEMPORARY_BASE', Files::getUnixStylePath(getcwd()) . '/Data/Temporary'); + define('FLOW_PATH_TEMPORARY_BASE', Files::getUnixStylePath($workingDirectory) . '/Data/Temporary'); } Files::createDirectoryRecursively('Configuration'); @@ -106,14 +110,19 @@ public static function postPackageUpdateAndInstall(PackageEvent $event): void */ protected static function copyDistributionFiles(string $installerResourcesDirectory): void { + $workingDirectory = getcwd(); + if ($workingDirectory === false) { + throw new \RuntimeException('Could not resolve working directory', 1744470280); + } + $essentialsPath = $installerResourcesDirectory . 'Distribution/Essentials'; if (is_dir($essentialsPath)) { - Files::copyDirectoryRecursively($essentialsPath, Files::getUnixStylePath(getcwd()) . '/', false, true); + Files::copyDirectoryRecursively($essentialsPath, Files::getUnixStylePath($workingDirectory) . '/', false, true); } $defaultsPath = $installerResourcesDirectory . 'Distribution/Defaults'; if (is_dir($defaultsPath)) { - Files::copyDirectoryRecursively($defaultsPath, Files::getUnixStylePath(getcwd()) . '/', true, true); + Files::copyDirectoryRecursively($defaultsPath, Files::getUnixStylePath($workingDirectory) . '/', true, true); } } @@ -127,7 +136,7 @@ protected static function copyDistributionFiles(string $installerResourcesDirect */ protected static function runPackageScripts(string $staticMethodReference, PackageEvent $event): void { - $className = substr($staticMethodReference, 0, strpos($staticMethodReference, '::')); + $className = substr($staticMethodReference, 0, strpos($staticMethodReference, '::') ?: 0); $methodName = substr($staticMethodReference, strpos($staticMethodReference, '::') + 2); if (!class_exists($className)) { diff --git a/Neos.Flow/Classes/Configuration/ConfigurationManager.php b/Neos.Flow/Classes/Configuration/ConfigurationManager.php index a9d0c092f8..454aadbb1c 100644 --- a/Neos.Flow/Classes/Configuration/ConfigurationManager.php +++ b/Neos.Flow/Classes/Configuration/ConfigurationManager.php @@ -135,7 +135,7 @@ class ConfigurationManager /** * Storage of the raw special configurations * - * @var array + * @var array */ protected $configurations = []; @@ -159,7 +159,7 @@ class ConfigurationManager protected $temporaryDirectoryPath = null; /** - * @var array + * @var array */ protected $unprocessedConfiguration = []; @@ -224,14 +224,10 @@ public function registerConfigurationType(string $configurationType, $configurat { if ($configurationLoader === null) { $configurationLoader = new MergeLoader(new YamlSource(), $configurationType); - - // B/C layer } elseif (is_string($configurationLoader)) { + // B/C layer $configurationLoader = $this->convertLegacyProcessingType($configurationType, $configurationLoader); } - if (!$configurationLoader instanceof LoaderInterface) { - throw new \InvalidArgumentException(sprintf('Specified invalid configuration loader of type "%s" while registering custom configuration type "%s". This should be an instance of %s', is_object($configurationLoader) ? get_class($configurationLoader) : gettype($configurationLoader), $configurationType, LoaderInterface::class), 1617895964); - } // if the configuration was already registered and the there is an unprocessed loaded configuration, the configuration needs to be loaded again // on the other hand, if there is a processed configuration loaded, but no unprocessed configuration, the config must be from the cache and is assumed to be valid @@ -308,7 +304,6 @@ public function getConfiguration(string $configurationType, ?string $configurati * This method writes the current configuration into a cache file if Flow was configured to do so. * * @return void - * @throws Exception\InvalidConfigurationException * @throws Exception\InvalidConfigurationTypeException * @throws FilesException */ @@ -419,7 +414,7 @@ public function flushConfigurationCache(): void * * @param string $configurationType The kind of configuration to fetch */ - protected function processConfigurationType(string $configurationType) + protected function processConfigurationType(string $configurationType): void { if ($this->temporaryDirectoryPath !== null) { // to avoid issues regarding concurrency and invalid filesystem characters @@ -524,7 +519,7 @@ protected function replaceVariablesInPhpString(string $phpString): string } $replacement .= ($constantDoesNotStartAsBeginning ? $matchGroup['startString'] . "' . " : '=> '); - if (isset($matchGroup['prefix']) && $matchGroup['prefix'] === 'env') { + if ($matchGroup['prefix'] === 'env') { if ($matchGroup['type'] === 'bool') { $replacement .= "!in_array(strtolower(getenv('" . $matchGroup['name'] . "')), ['', '0', 'false'], true)"; } elseif ($matchGroup['type'] !== '') { @@ -532,7 +527,7 @@ protected function replaceVariablesInPhpString(string $phpString): string } else { $replacement .= "getenv('" . $matchGroup['name'] . "')"; } - } elseif (isset($matchGroup['expression'])) { + } else { $replacement .= "(defined('" . $matchGroup['expression'] . "') ? constant('" . $matchGroup['expression'] . "') : null)"; } @@ -548,7 +543,11 @@ protected function replaceVariablesInPhpString(string $phpString): string return $replacement; }, $phpString); - return $phpString; + if (is_string($phpString)) { + return $phpString; + } + + throw new \RuntimeException('Failed to render PHP string', 1744468312); } /** diff --git a/Neos.Flow/Classes/Configuration/ConfigurationSchemaValidator.php b/Neos.Flow/Classes/Configuration/ConfigurationSchemaValidator.php index 279c0bee90..1b2d70e599 100644 --- a/Neos.Flow/Classes/Configuration/ConfigurationSchemaValidator.php +++ b/Neos.Flow/Classes/Configuration/ConfigurationSchemaValidator.php @@ -64,9 +64,9 @@ class ConfigurationSchemaValidator /** * Validate the given $configurationType and $path * - * @param string $configurationType (optional) the configuration type to validate. if NULL, validates all configuration. - * @param string $path (optional) configuration path to validate - * @param array $loadedSchemaFiles (optional). if given, will be filled with a list of loaded schema files + * @param ?string $configurationType (optional) the configuration type to validate. if NULL, validates all configuration. + * @param ?string $path (optional) configuration path to validate + * @param array $loadedSchemaFiles (optional). if given, will be filled with a list of loaded schema files * @return \Neos\Error\Messages\Result the result of the validation * @throws Exception\SchemaValidationException */ @@ -91,7 +91,7 @@ public function validate(?string $configurationType = null, ?string $path = null * * @param string $configurationType the configuration typr to validate * @param string|null $path configuration path to validate, or NULL. - * @param array $loadedSchemaFiles will be filled with a list of loaded schema files + * @param array $loadedSchemaFiles will be filled with a list of loaded schema files * @return \Neos\Error\Messages\Result * @throws Exception\SchemaValidationException */ @@ -121,7 +121,7 @@ protected function validateSingleType(string $configurationType, ?string $path = $schemaType = $schemaNameParts[0]; $schemaPath = isset($schemaNameParts[1]) ? $schemaNameParts[1] : null; - if ($schemaType === $configurationType && ($path === null || strpos($schemaPath, $path) === 0)) { + if ($schemaPath && $schemaType === $configurationType && ($path === null || str_starts_with($schemaPath, $path))) { $schemaFileInfos[] = [ 'file' => $schemaFile, 'name' => $schemaName, @@ -138,6 +138,7 @@ protected function validateSingleType(string $configurationType, ?string $path = } $result = new Result(); + /** @var array $schemaFileInfos */ foreach ($schemaFileInfos as $schemaFileInfo) { $loadedSchemaFiles[] = $schemaFileInfo['file']; diff --git a/Neos.Flow/Classes/Configuration/Loader/AppendLoader.php b/Neos.Flow/Classes/Configuration/Loader/AppendLoader.php index c9de87c21c..d0cf76e366 100644 --- a/Neos.Flow/Classes/Configuration/Loader/AppendLoader.php +++ b/Neos.Flow/Classes/Configuration/Loader/AppendLoader.php @@ -15,6 +15,7 @@ use Neos\Flow\Configuration\Source\YamlSource; use Neos\Flow\Core\ApplicationContext; +use Neos\Flow\Package\FlowPackageInterface; class AppendLoader implements LoaderInterface { @@ -34,6 +35,10 @@ public function __construct(YamlSource $yamlSource, string $filePrefix) $this->filePrefix = $filePrefix; } + /** + * @param array $packages + * @return array> + */ public function load(array $packages, ApplicationContext $context): array { $configuration = []; diff --git a/Neos.Flow/Classes/Configuration/Loader/LoaderInterface.php b/Neos.Flow/Classes/Configuration/Loader/LoaderInterface.php index 2a43031b37..a2dbe8f0ce 100644 --- a/Neos.Flow/Classes/Configuration/Loader/LoaderInterface.php +++ b/Neos.Flow/Classes/Configuration/Loader/LoaderInterface.php @@ -14,6 +14,7 @@ */ use Neos\Flow\Core\ApplicationContext; +use Neos\Flow\Package\PackageInterface; /** * The interface for a configuration loader @@ -23,9 +24,9 @@ interface LoaderInterface /** * Read configuration resources and return the final configuration array for the given configurationType * - * @param array $packages An array of Package objects (indexed by package key) to consider + * @param array $packages An array of Package objects (indexed by package key) to consider * @param ApplicationContext $context - * @return array The Configuration array for the current configurationType + * @return array The Configuration array for the current configurationType */ public function load(array $packages, ApplicationContext $context) : array; } diff --git a/Neos.Flow/Classes/Configuration/Loader/MergeLoader.php b/Neos.Flow/Classes/Configuration/Loader/MergeLoader.php index 322573ce3a..67a9470c53 100644 --- a/Neos.Flow/Classes/Configuration/Loader/MergeLoader.php +++ b/Neos.Flow/Classes/Configuration/Loader/MergeLoader.php @@ -15,6 +15,7 @@ use Neos\Flow\Configuration\Source\YamlSource; use Neos\Flow\Core\ApplicationContext; +use Neos\Flow\Package\FlowPackageInterface; use Neos\Utility\Arrays; class MergeLoader implements LoaderInterface @@ -35,6 +36,10 @@ public function __construct(YamlSource $yamlSource, string $filePrefix) $this->filePrefix = $filePrefix; } + /** + * @param array $packages + * @return array + */ public function load(array $packages, ApplicationContext $context): array { $configuration = []; diff --git a/Neos.Flow/Classes/Configuration/Loader/ObjectsLoader.php b/Neos.Flow/Classes/Configuration/Loader/ObjectsLoader.php index ec0fb02c55..821f023554 100644 --- a/Neos.Flow/Classes/Configuration/Loader/ObjectsLoader.php +++ b/Neos.Flow/Classes/Configuration/Loader/ObjectsLoader.php @@ -16,6 +16,7 @@ use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Configuration\Source\YamlSource; use Neos\Flow\Core\ApplicationContext; +use Neos\Flow\Package\FlowPackageInterface; use Neos\Utility\Arrays; class ObjectsLoader implements LoaderInterface @@ -30,6 +31,10 @@ public function __construct(YamlSource $yamlSource) $this->yamlSource = $yamlSource; } + /** + * @param array $packages + * @return array> + */ public function load(array $packages, ApplicationContext $context): array { $configuration = []; diff --git a/Neos.Flow/Classes/Configuration/Loader/PolicyLoader.php b/Neos.Flow/Classes/Configuration/Loader/PolicyLoader.php index 05fcae91c7..9c2a4b33de 100644 --- a/Neos.Flow/Classes/Configuration/Loader/PolicyLoader.php +++ b/Neos.Flow/Classes/Configuration/Loader/PolicyLoader.php @@ -16,8 +16,12 @@ use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Configuration\Source\YamlSource; use Neos\Flow\Core\ApplicationContext; +use Neos\Flow\Package\FlowPackageInterface; use Neos\Utility\Arrays; +/** + * @phpstan-type PolicyDefinition array{roles?: array}>} + */ class PolicyLoader implements LoaderInterface { /** @@ -47,6 +51,10 @@ public function setTemporaryDirectoryPath(string $temporaryDirectoryPath): void $this->temporaryDirectoryPath = $temporaryDirectoryPath; } + /** + * @param array $packages + * @return PolicyDefinition + */ public function load(array $packages, ApplicationContext $context): array { if ($context->isTesting()) { @@ -77,9 +85,9 @@ public function load(array $packages, ApplicationContext $context): array /** * Merges two policy configuration arrays. * - * @param array $firstConfigurationArray - * @param array $secondConfigurationArray - * @return array + * @param PolicyDefinition $firstConfigurationArray + * @param PolicyDefinition $secondConfigurationArray + * @return PolicyDefinition */ private function mergePolicyConfiguration(array $firstConfigurationArray, array $secondConfigurationArray): array { diff --git a/Neos.Flow/Classes/Configuration/Loader/RoutesLoader.php b/Neos.Flow/Classes/Configuration/Loader/RoutesLoader.php index c3f5fb8da4..37efd6307a 100644 --- a/Neos.Flow/Classes/Configuration/Loader/RoutesLoader.php +++ b/Neos.Flow/Classes/Configuration/Loader/RoutesLoader.php @@ -26,6 +26,26 @@ use Neos\Utility\Arrays; use Neos\Utility\PositionalArraySorter; +/** + * @phpstan-type RouteDefinition array{ + * name: string, + * providerFactory?: class-string, + * providerOptions?: array, + * uriPattern?: string, + * subRoutes?: array, + * suffix?: string, + * }> + * } + * @phpstan-type SubrouteDefinition array{ + * name?: string, + * uriPattern?: string, + * defaults?: mixed, + * routeParts?: mixed, + * subRoutes?: mixed, + * } + */ class RoutesLoader implements LoaderInterface { /** @@ -52,9 +72,9 @@ public function __construct(YamlSource $yamlSource, ConfigurationManager $config } /** - * @param array $packages + * @param array $packages * @param ApplicationContext $context - * @return array + * @return array> * @throws ConfigurationException | InvalidConfigurationException | InvalidConfigurationTypeException | ParseErrorException | RecursionException */ public function load(array $packages, ApplicationContext $context): array @@ -77,9 +97,9 @@ public function load(array $packages, ApplicationContext $context): array * Merges routes from Neos.Flow.mvc.routes settings into $routeDefinitions * NOTE: Routes from settings will always be appended to existing route definitions from the main Routes configuration! * - * @param array $routeDefinitions - * @param array $routeSettings - * @return array + * @param array $routeDefinitions + * @param array $routeSettings + * @return array */ protected function includeSubRoutesFromSettings(array $routeDefinitions, array $routeSettings): array { @@ -121,9 +141,9 @@ protected function includeSubRoutesFromSettings(array $routeDefinitions, array $ * * @param PackageInterface[] $packages * @param ApplicationContext $context - * @param array $routesConfiguration + * @param array $routesConfiguration * @param int $subRoutesRecursionLevel Counts how many SubRoutes have been loaded. If this number exceeds MAXIMUM_SUBROUTE_RECURSIONS, an exception is thrown - * @return array + * @return array> * @throws ParseErrorException | RecursionException| ConfigurationException */ protected function mergeRoutesWithSubRoutes(array $packages, ApplicationContext $context, array $routesConfiguration, int $subRoutesRecursionLevel = 0): array @@ -176,11 +196,11 @@ protected function mergeRoutesWithSubRoutes(array $packages, ApplicationContext /** * Merges all routes in $routesConfiguration with the sub routes in $subRoutesConfiguration * - * @param array $routesConfiguration - * @param array $subRoutesConfiguration + * @param array $routesConfiguration + * @param array $subRoutesConfiguration * @param string $subRouteKey the key of the sub route: - * @param array $subRouteOptions - * @return array the merged route configuration + * @param array{variables?: array} $subRouteOptions + * @return array the merged route configuration * @throws ParseErrorException */ protected function buildSubRouteConfigurations(array $routesConfiguration, array $subRoutesConfiguration, string $subRouteKey, array $subRouteOptions): array @@ -190,7 +210,11 @@ protected function buildSubRouteConfigurations(array $routesConfiguration, array foreach ($subRoutesConfiguration as $subRouteConfiguration) { foreach ($routesConfiguration as $routeConfiguration) { $mergedSubRouteConfiguration = $subRouteConfiguration; - $mergedSubRouteConfiguration['name'] = sprintf('%s :: %s', $routeConfiguration['name'] ?? 'Unnamed Route', $subRouteConfiguration['name'] ?? 'Unnamed Subroute'); + $mergedSubRouteConfiguration['name'] = sprintf( + '%s :: %s', + $routeConfiguration['name'] ?? 'Unnamed Route', + $subRouteConfiguration['name'] ?? 'Unnamed Subroute' + ); $mergedSubRouteConfiguration['name'] = $this->replacePlaceholders($mergedSubRouteConfiguration['name'], $variables); if (!isset($mergedSubRouteConfiguration['uriPattern'])) { throw new ParseErrorException('No uriPattern defined in route configuration "' . $mergedSubRouteConfiguration['name'] . '".', 1274197615); @@ -207,6 +231,7 @@ protected function buildSubRouteConfigurations(array $routesConfiguration, array if (isset($mergedSubRouteConfiguration['routeParts'])) { $mergedSubRouteConfiguration['routeParts'] = $this->replacePlaceholders($mergedSubRouteConfiguration['routeParts'], $variables); } + /** @var SubrouteDefinition $mergedSubRouteConfiguration arrayMergeRecursiveOverrule does not change that */ $mergedSubRouteConfiguration = Arrays::arrayMergeRecursiveOverrule($routeConfiguration, $mergedSubRouteConfiguration); unset($mergedSubRouteConfiguration['subRoutes']); $mergedSubRoutesConfigurations[] = $mergedSubRouteConfiguration; @@ -219,9 +244,10 @@ protected function buildSubRouteConfigurations(array $routesConfiguration, array /** * Replaces placeholders in the format with the corresponding variable of the specified $variables collection. * - * @param string|array $value - * @param array $variables - * @return array|string + * @param string|array $value + * @param array $variables + * + * @return ($value is string ? string : array) */ private function replacePlaceholders($value, array $variables) { diff --git a/Neos.Flow/Classes/Configuration/Loader/SettingsLoader.php b/Neos.Flow/Classes/Configuration/Loader/SettingsLoader.php index c5d08f3524..1d136aa3cc 100644 --- a/Neos.Flow/Classes/Configuration/Loader/SettingsLoader.php +++ b/Neos.Flow/Classes/Configuration/Loader/SettingsLoader.php @@ -16,6 +16,7 @@ use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Configuration\Source\YamlSource; use Neos\Flow\Core\ApplicationContext; +use Neos\Flow\Package\FlowPackageInterface; use Neos\Utility\Arrays; class SettingsLoader implements LoaderInterface @@ -30,6 +31,10 @@ public function __construct(YamlSource $yamlSource) $this->yamlSource = $yamlSource; } + /** + * @param array $packages + * @return array + */ public function load(array $packages, ApplicationContext $context): array { // Make sure that the Flow package is the first item of the packages array: diff --git a/Neos.Flow/Classes/Configuration/Source/YamlSource.php b/Neos.Flow/Classes/Configuration/Source/YamlSource.php index 7bcbd946bb..019df55f1d 100644 --- a/Neos.Flow/Classes/Configuration/Source/YamlSource.php +++ b/Neos.Flow/Classes/Configuration/Source/YamlSource.php @@ -76,7 +76,7 @@ public function has(string $pathAndFilename, bool $allowSplitSource = false): bo * * @param string $pathAndFilename Full path and filename of the file to load, excluding the file extension (ie. ".yaml") * @param boolean $allowSplitSource If true, the type will be used as a prefix when looking for configuration files - * @return array + * @return array * @throws ParseErrorException * @throws \Neos\Flow\Configuration\Exception */ @@ -85,9 +85,9 @@ public function load(string $pathAndFilename, bool $allowSplitSource = false): a $this->detectFilesWithWrongExtension($pathAndFilename, $allowSplitSource); $pathsAndFileNames = [$pathAndFilename . '.yaml']; if ($allowSplitSource === true) { - $splitSourcePathsAndFileNames = glob($pathAndFilename . '.*.yaml'); - $splitSourcePathsAndFileNames = array_merge($splitSourcePathsAndFileNames, glob($pathAndFilename . '.*.yml')); - if ($splitSourcePathsAndFileNames !== false) { + $splitSourcePathsAndFileNames = glob($pathAndFilename . '.*.yaml') ?: []; + $splitSourcePathsAndFileNames = array_merge($splitSourcePathsAndFileNames, glob($pathAndFilename . '.*.yml') ?: []); + if ($splitSourcePathsAndFileNames != []) { sort($splitSourcePathsAndFileNames); $pathsAndFileNames = array_merge($pathsAndFileNames, $splitSourcePathsAndFileNames); } @@ -104,7 +104,7 @@ public function load(string $pathAndFilename, bool $allowSplitSource = false): a * @param bool $allowSplitSource * @throws \Neos\Flow\Configuration\Exception */ - protected function detectFilesWithWrongExtension($pathAndFilename, $allowSplitSource = false) + protected function detectFilesWithWrongExtension($pathAndFilename, $allowSplitSource = false): void { $wrongPathsAndFileNames = []; if (is_file($pathAndFilename . '.yml')) { @@ -125,8 +125,8 @@ protected function detectFilesWithWrongExtension($pathAndFilename, $allowSplitSo * Loads the file with the given path and merge it's contents into the configuration array. * * @param string $pathAndFilename - * @param array $configuration - * @return array + * @param array $configuration + * @return array * @throws ParseErrorException */ protected function mergeFileContent(string $pathAndFilename, array $configuration): array @@ -137,6 +137,9 @@ protected function mergeFileContent(string $pathAndFilename, array $configuratio try { $yaml = file_get_contents($pathAndFilename); + if ($yaml === false) { + throw new ParseErrorException('Failed to load content from file "' . $pathAndFilename . '".', 1744463546); + } if ($this->usePhpYamlExtension) { $loadedConfiguration = @yaml_parse($yaml); if ($loadedConfiguration === false) { @@ -165,7 +168,7 @@ protected function mergeFileContent(string $pathAndFilename, array $configuratio * Save the specified configuration array to the given file in YAML format. * * @param string $pathAndFilename Full path and filename of the file to write to, excluding the dot and file extension (i.e. ".yaml") - * @param array $configuration The configuration to save + * @param array $configuration The configuration to save * @return void */ public function save(string $pathAndFilename, array $configuration) @@ -189,14 +192,16 @@ protected function getHeaderFromFile(string $pathAndFilename): string { $header = ''; $fileHandle = fopen($pathAndFilename, 'r'); - while ($line = fgets($fileHandle)) { - if (preg_match('/^#/', $line)) { - $header .= $line; - } else { - break; + if ($fileHandle !== false) { + while ($line = fgets($fileHandle)) { + if (preg_match('/^#/', $line)) { + $header .= $line; + } else { + break; + } } + fclose($fileHandle); } - fclose($fileHandle); return $header; } } diff --git a/Neos.Flow/Classes/Core/Booting/Scripts.php b/Neos.Flow/Classes/Core/Booting/Scripts.php index 74edfed2e0..50ffe4cb57 100644 --- a/Neos.Flow/Classes/Core/Booting/Scripts.php +++ b/Neos.Flow/Classes/Core/Booting/Scripts.php @@ -13,6 +13,8 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use Neos\Cache\CacheFactoryInterface; +use Neos\Cache\Frontend\PhpFrontend; +use Neos\Cache\Frontend\VariableFrontend; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cache\CacheFactory; use Neos\Flow\Cache\CacheManager; @@ -30,6 +32,7 @@ use Neos\Flow\Core\ProxyClassLoader; use Neos\Flow\Error\Debugger; use Neos\Flow\Error\ErrorHandler; +use Neos\Flow\Error\ExceptionHandlerInterface; use Neos\Flow\Error\ProductionExceptionHandler; use Neos\Flow\Http\Helper\RequestInformationHelper; use Neos\Flow\Http\HttpRequestHandlerInterface; @@ -40,8 +43,11 @@ use Neos\Flow\ObjectManagement\CompileTimeObjectManager; use Neos\Flow\ObjectManagement\ObjectManager; use Neos\Flow\ObjectManagement\ObjectManagerInterface; +use Neos\Flow\Package; use Neos\Flow\Package\FlowPackageInterface; use Neos\Flow\Package\GenericPackage; +use Neos\Flow\Package\PackageInterface; +use Neos\Flow\Package\PackageKeyAwareInterface; use Neos\Flow\Package\PackageManager; use Neos\Flow\Reflection\ReflectionService; use Neos\Flow\Reflection\ReflectionServiceFactory; @@ -51,7 +57,6 @@ use Neos\Utility\Files; use Neos\Utility\OpcodeCacheHelper; use Neos\Flow\Exception as FlowException; -use Psr\Http\Message\RequestInterface; /** * Initialization scripts for modules of the Flow package @@ -73,7 +78,8 @@ class Scripts public static function initializeClassLoader(Bootstrap $bootstrap) { $proxyClassLoader = new ProxyClassLoader($bootstrap->getContext()); - spl_autoload_register([$proxyClassLoader, 'loadClass'], true, true); + /** @phpstan-ignore argument.type (we return boolean in the closure but void is expected) */ + spl_autoload_register($proxyClassLoader->loadClass(...), true, true); $bootstrap->setEarlyInstance(ProxyClassLoader::class, $proxyClassLoader); if (!self::useClassLoader($bootstrap)) { @@ -94,7 +100,8 @@ public static function initializeClassLoader(Bootstrap $bootstrap) ]; $classLoader = new ClassLoader($initialClassLoaderMappings); - spl_autoload_register([$classLoader, 'loadClass'], true); + /** @phpstan-ignore argument.type (we return boolean in the closure but void is expected) */ + spl_autoload_register($classLoader->loadClass(...), true); $bootstrap->setEarlyInstance(ClassLoader::class, $classLoader); $classLoader->setConsiderTestsNamespace(true); } @@ -123,6 +130,9 @@ public static function registerClassLoaderInAnnotationRegistry(Bootstrap $bootst public static function initializeClassLoaderClassesCache(Bootstrap $bootstrap) { $classesCache = $bootstrap->getEarlyInstance(CacheManager::class)->getCache('Flow_Object_Classes'); + if (!$classesCache instanceof PhpFrontend) { + throw new \Exception('The classes cache must be a PhpFrontend', 1744461618); + } $bootstrap->getEarlyInstance(ProxyClassLoader::class)->injectClassesCache($classesCache); } @@ -185,7 +195,9 @@ public static function initializePackageManagement(Bootstrap $bootstrap) $packageManager->initialize($bootstrap); if (self::useClassLoader($bootstrap)) { - $bootstrap->getEarlyInstance(ClassLoader::class)->setPackages($packageManager->getAvailablePackages()); + $bootstrap->getEarlyInstance(ClassLoader::class)->setPackages( + $packageManager->getAvailablePackages() + ); } } @@ -260,7 +272,15 @@ public static function initializeSystemLogger(Bootstrap $bootstrap): void * Initialize the exception storage * * @param Bootstrap $bootstrap - * @param array $settings The Neos.Flow settings + * @param array{ + * log?: array{ + * throwables?: array{ + * storageClass?: class-string, + * optionsByImplementation?: array, array>, + * renderRequestInformation?: bool, + * } + * } + * } $settings The Neos.Flow settings * @return ThrowableStorageInterface * @throws FlowException * @throws InvalidConfigurationTypeException @@ -271,7 +291,6 @@ protected static function initializeExceptionStorage(Bootstrap $bootstrap, array $storageOptions = $settings['log']['throwables']['optionsByImplementation'][$storageClassName] ?? []; $renderRequestInformation = $settings['log']['throwables']['renderRequestInformation'] ?? true; - if (!in_array(ThrowableStorageInterface::class, class_implements($storageClassName, true))) { throw new \Exception( sprintf('The class "%s" configured as throwable storage does not implement the ThrowableStorageInterface', $storageClassName), @@ -304,7 +323,7 @@ protected static function initializeExceptionStorage(Bootstrap $bootstrap, array $request = $requestHandler->getHttpRequest(); if ($renderRequestInformation) { - $output .= PHP_EOL . 'HTTP REQUEST:' . PHP_EOL . ($request instanceof RequestInterface ? RequestInformationHelper::renderRequestInformation($request) : '[request was empty]') . PHP_EOL; + $output .= PHP_EOL . 'HTTP REQUEST:' . PHP_EOL . RequestInformationHelper::renderRequestInformation($request) . PHP_EOL; } $output .= PHP_EOL . 'PHP PROCESS:' . PHP_EOL . 'Inode: ' . getmyinode() . PHP_EOL . 'PID: ' . getmypid() . PHP_EOL . 'UID: ' . getmyuid() . PHP_EOL . 'GID: ' . getmygid() . PHP_EOL . 'User: ' . get_current_user() . PHP_EOL; @@ -325,11 +344,25 @@ protected static function initializeExceptionStorage(Bootstrap $bootstrap, array public static function initializeErrorHandling(Bootstrap $bootstrap) { $configurationManager = $bootstrap->getEarlyInstance(ConfigurationManager::class); + /** + * @var array{ + * error: array{ + * errorHandler: array{ + * exceptionalErrors: array, + * }, + * exceptionHandler: array{ + * className: class-string, + * } + * } + * } $settings + */ $settings = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Neos.Flow'); $errorHandler = new ErrorHandler(); $errorHandler->setExceptionalErrors($settings['error']['errorHandler']['exceptionalErrors']); - $exceptionHandler = class_exists($settings['error']['exceptionHandler']['className']) ? new $settings['error']['exceptionHandler']['className'] : new ProductionExceptionHandler(); + $exceptionHandler = class_exists($settings['error']['exceptionHandler']['className']) + ? new $settings['error']['exceptionHandler']['className'] + : new ProductionExceptionHandler(); if (is_callable([$exceptionHandler, 'injectLogger'])) { $exceptionHandler->injectLogger($bootstrap->getEarlyInstance(PsrLoggerFactoryInterface::class)->get('systemLogger')); @@ -415,13 +448,18 @@ public static function initializeProxyClasses(Bootstrap $bootstrap) $proxyClassLoader->initializeAvailableProxyClasses($bootstrap->getContext()); // Check if code was updated, if not something went wrong + /** @phpstan-ignore identical.alwaysTrue (could have changed on code update) */ if ($objectConfigurationCache->has('allCompiledCodeUpToDate') === false) { if (DIRECTORY_SEPARATOR === '/') { $phpBinaryPathAndFilename = '"' . escapeshellcmd(Files::getUnixStylePath($settings['core']['phpBinaryPathAndFilename'])) . '"'; } else { $phpBinaryPathAndFilename = escapeshellarg(Files::getUnixStylePath($settings['core']['phpBinaryPathAndFilename'])); } - $command = sprintf('%s -c %s -v', $phpBinaryPathAndFilename, escapeshellarg(php_ini_loaded_file())); + $iniPath = php_ini_loaded_file(); + if ($iniPath === false) { + throw new \Exception('Failed to resolve PHP ini path', 1744460978); + } + $command = sprintf('%s -c %s -v', $phpBinaryPathAndFilename, escapeshellarg($iniPath)); exec($command, $output, $result); if ($result !== 0) { if (!file_exists($phpBinaryPathAndFilename)) { @@ -452,7 +490,7 @@ public static function recompileClasses(Bootstrap $bootstrap) * * @param Bootstrap $bootstrap */ - public static function initializeObjectManagerCompileTimeCreate(Bootstrap $bootstrap) + public static function initializeObjectManagerCompileTimeCreate(Bootstrap $bootstrap): void { $objectManager = new CompileTimeObjectManager($bootstrap->getContext()); $bootstrap->setEarlyInstance(ObjectManagerInterface::class, $objectManager); @@ -475,7 +513,7 @@ public static function initializeObjectManagerCompileTimeCreate(Bootstrap $boots */ public static function initializeObjectManagerCompileTimeFinalize(Bootstrap $bootstrap) { - /** @var CompileTimeObjectManager $objectManager */ + /** @var CompileTimeObjectManager $objectManager @phpstan-ignore missingType.generics */ $objectManager = $bootstrap->getObjectManager(); $configurationManager = $bootstrap->getEarlyInstance(ConfigurationManager::class); $reflectionService = $objectManager->get(ReflectionService::class); @@ -486,7 +524,11 @@ public static function initializeObjectManagerCompileTimeFinalize(Bootstrap $boo $objectManager->injectAllSettings($configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS)); $objectManager->injectReflectionService($reflectionService); $objectManager->injectConfigurationManager($configurationManager); - $objectManager->injectConfigurationCache($cacheManager->getCache('Flow_Object_Configuration')); + $configurationCache = $cacheManager->getCache('Flow_Object_Configuration'); + if (!$configurationCache instanceof VariableFrontend) { + throw new \Exception('Configuration cache must be of type VariableFrontend', 1744459785); + } + $objectManager->injectConfigurationCache($configurationCache); $objectManager->injectLogger($logger); $objectManager->initialize($packageManager->getAvailablePackages()); @@ -543,7 +585,7 @@ public static function initializeReflectionServiceFactory(Bootstrap $bootstrap) * @param Bootstrap $bootstrap * @throws FlowException */ - public static function initializeReflectionService(Bootstrap $bootstrap) + public static function initializeReflectionService(Bootstrap $bootstrap): void { $reflectionService = $bootstrap->getEarlyInstance(ReflectionServiceFactory::class)->create(); $bootstrap->setEarlyInstance(ReflectionService::class, $reflectionService); @@ -607,12 +649,12 @@ public static function initializeSystemFileMonitor(Bootstrap $bootstrap) /** * @param Bootstrap $bootstrap - * @return array + * @return array */ protected static function getListOfPackagesWithConfiguredObjects(Bootstrap $bootstrap): array { $objectManager = $bootstrap->getObjectManager(); - /** @phpstan-ignore-next-line the object manager interface doesn't specify this method */ + /** @phpstan-ignore method.notFound (not part of the interface) */ $allObjectConfigurations = $objectManager->getAllObjectConfigurations(); $packagesWithConfiguredObjects = array_reduce($allObjectConfigurations, function ($foundPackages, $item) { if (isset($item['p']) && !in_array($item['p'], $foundPackages)) { @@ -683,9 +725,17 @@ public static function initializeResources(Bootstrap $bootstrap) * Executes the given command as a sub-request to the Flow CLI system. * * @param string $commandIdentifier E.g. neos.flow:cache:flush - * @param array $settings The Neos.Flow settings + * @param array{ + * core: array{ + * context: string, + * phpBinaryPathAndFilename: string, + * subRequestPhpIniPathAndFilename?: mixed, + * subRequestEnvironmentVariables?: array, + * subRequestIniEntries?: array, + * }, + * } $settings The Neos.Flow settings * @param boolean $outputResults Echo the commands output on success - * @param array $commandArguments Command arguments + * @param array $commandArguments Command arguments * @return true Legacy return value. Will always be true. A failure is expressed as a thrown exception * @throws Exception\SubProcessException The execution of the sub process failed * @api @@ -712,7 +762,7 @@ public static function executeCommand(string $commandIdentifier, array $settings } if (file_exists(FLOW_PATH_DATA . 'Logs/Exceptions') && is_dir(FLOW_PATH_DATA . 'Logs/Exceptions') && is_writable(FLOW_PATH_DATA . 'Logs/Exceptions')) { // Logs the command string `php ./flow foo:bar` inside `Logs/Exceptions/123-command.txt` - $referenceCode = date('YmdHis', $_SERVER['REQUEST_TIME']) . substr(md5(rand()), 0, 6); + $referenceCode = date('YmdHis', $_SERVER['REQUEST_TIME']) . substr(md5((string)rand()), 0, 6); $errorDumpPathAndFilename = FLOW_PATH_DATA . 'Logs/Exceptions/' . $referenceCode . '-command.txt'; file_put_contents($errorDumpPathAndFilename, $command); $exceptionMessage .= sprintf(' It has been stored in: %s', basename($errorDumpPathAndFilename)); @@ -735,8 +785,16 @@ public static function executeCommand(string $commandIdentifier, array $settings * Note: As the command execution is done in a separate thread potential exceptions or failures will *not* be reported * * @param string $commandIdentifier E.g. neos.flow:cache:flush - * @param array $settings The Neos.Flow settings - * @param array $commandArguments Command arguments + * @param array{ + * core: array{ + * context: string, + * phpBinaryPathAndFilename: string, + * subRequestPhpIniPathAndFilename?: mixed, + * subRequestEnvironmentVariables?: array, + * subRequestIniEntries?: array, + * }, + * } $settings The Neos.Flow settings + * @param array $commandArguments Command arguments * @return void * @api */ @@ -746,14 +804,25 @@ public static function executeCommandAsync(string $commandIdentifier, array $set if (DIRECTORY_SEPARATOR === '/') { exec($command . ' > /dev/null 2>/dev/null &'); } else { - pclose(popen('START /B CMD /S /C "' . $command . '" > NUL 2> NUL &', 'r')); + $handle = popen('START /B CMD /S /C "' . $command . '" > NUL 2> NUL &', 'r'); + if ($handle) { + pclose($handle); + } } } /** * @param string $commandIdentifier E.g. neos.flow:cache:flush - * @param array $settings The Neos.Flow settings - * @param array $commandArguments Command arguments + * @param array{ + * core: array{ + * context: string, + * phpBinaryPathAndFilename: string, + * subRequestPhpIniPathAndFilename?: mixed, + * subRequestEnvironmentVariables?: array, + * subRequestIniEntries?: mixed, + * }, + * } $settings The Neos.Flow settings + * @param array $commandArguments Command arguments * @return string A command line command ready for being exec()uted */ protected static function buildSubprocessCommand(string $commandIdentifier, array $settings, array $commandArguments = []): string @@ -781,7 +850,14 @@ protected static function buildSubprocessCommand(string $commandIdentifier, arra } /** - * @param array $settings The Neos.Flow settings + * @param array{ + * core: array{ + * context: string, + * phpBinaryPathAndFilename: string, + * subRequestPhpIniPathAndFilename?: mixed, + * subRequestEnvironmentVariables?: array, + * }, + * } $settings The Neos.Flow settings * @return string A command line command for PHP, which can be extended and then exec()uted * @throws Exception\SubProcessException in case the phpBinaryPathAndFilename is incorrect */ @@ -823,6 +899,9 @@ public static function buildPhpCommand(array $settings): string } else { $useIniFile = $settings['core']['subRequestPhpIniPathAndFilename']; } + if (!is_string($useIniFile)) { + throw new \Exception('Could not resolve ini file', 1744460624); + } $command .= ' -c ' . escapeshellarg($useIniFile); } @@ -838,7 +917,7 @@ public static function buildPhpCommand(array $settings): string * @param string $phpBinaryPathAndFilename * @throws Exception\SubProcessException in case the php binary doesn't exist / is a different one for the current cli request */ - protected static function ensureCLISubrequestsUseCurrentlyRunningPhpBinary($phpBinaryPathAndFilename) + protected static function ensureCLISubrequestsUseCurrentlyRunningPhpBinary($phpBinaryPathAndFilename): void { // Do nothing for non-CLI requests if (PHP_SAPI !== 'cli') { @@ -846,7 +925,7 @@ protected static function ensureCLISubrequestsUseCurrentlyRunningPhpBinary($phpB } // Ensure the actual PHP binary is known before checking if it is correct. - if (!$phpBinaryPathAndFilename || strlen($phpBinaryPathAndFilename) === 0) { + if (!$phpBinaryPathAndFilename) { throw new Exception\SubProcessException('"Neos.Flow.core.phpBinaryPathAndFilename" is not set.', 1689676816060); } @@ -915,7 +994,7 @@ protected static function ensureCLISubrequestsUseCurrentlyRunningPhpBinary($phpB * @param string $phpCommand the completely build php string that is used to execute subrequests * @throws Exception\SubProcessException in case the php binary doesn't exist, or is not suitable for cli usage, or its version doesn't match */ - protected static function ensureWebSubrequestsUseCurrentlyRunningPhpVersion($phpCommand) + protected static function ensureWebSubrequestsUseCurrentlyRunningPhpVersion($phpCommand): void { // Do nothing for CLI requests if (PHP_SAPI === 'cli') { diff --git a/Neos.Flow/Classes/Core/Booting/Sequence.php b/Neos.Flow/Classes/Core/Booting/Sequence.php index 99999e236b..fd325adcea 100644 --- a/Neos.Flow/Classes/Core/Booting/Sequence.php +++ b/Neos.Flow/Classes/Core/Booting/Sequence.php @@ -28,7 +28,7 @@ class Sequence protected $identifier; /** - * @var array + * @var array> */ protected $steps = []; diff --git a/Neos.Flow/Classes/Core/Bootstrap.php b/Neos.Flow/Classes/Core/Bootstrap.php index 5d1848d51b..9eddf0b8f6 100644 --- a/Neos.Flow/Classes/Core/Bootstrap.php +++ b/Neos.Flow/Classes/Core/Bootstrap.php @@ -57,7 +57,7 @@ class Bootstrap /** * The same instance like $objectManager, but static, for use in the proxy classes. * - * @var ObjectManagerInterface + * @var ?ObjectManagerInterface */ public static $staticObjectManager; @@ -164,7 +164,7 @@ public function registerRequestHandler(RequestHandlerInterface $requestHandler) * * @param class-string $className */ - public function setPreselectedRequestHandlerClassName(string $className) + public function setPreselectedRequestHandlerClassName(string $className): void { $this->preselectedRequestHandlerClassName = $className; } @@ -492,9 +492,21 @@ protected function defineConstants() } } if ($rootPath !== false) { - $rootPath = Files::getUnixStylePath(realpath($rootPath)) . '/'; - $testPath = Files::getUnixStylePath(realpath(Files::concatenatePaths([$rootPath, 'Packages/Framework/Neos.Flow']))) . '/'; - $expectedPath = Files::getUnixStylePath(realpath(FLOW_PATH_FLOW)) . '/'; + $realRootPath = realpath($rootPath); + if ($realRootPath === false) { + throw new \RuntimeException('Failed to resolve real root path', 1744459165); + } + $rootPath = Files::getUnixStylePath($realRootPath) . '/'; + $realTestPath = realpath(Files::concatenatePaths([$rootPath, 'Packages/Framework/Neos.Flow'])); + if ($realTestPath === false) { + throw new \RuntimeException('Failed to resolve real test path', 1744459220); + } + $testPath = Files::getUnixStylePath($realTestPath) . '/'; + $realExpectedPath = realpath(FLOW_PATH_FLOW); + if ($realExpectedPath === false) { + throw new \RuntimeException('Failed to resolve real expected path', 1744459246); + } + $expectedPath = Files::getUnixStylePath($realExpectedPath) . '/'; if ($testPath !== $expectedPath) { echo('Flow: Invalid root path. (Error #1248964375)' . PHP_EOL . '"' . $testPath . '" does not lead to' . PHP_EOL . '"' . $expectedPath . '"' . PHP_EOL); http_response_code(500); @@ -513,16 +525,28 @@ protected function defineConstants() } if (!defined('FLOW_PATH_WEB')) { if (isset($_SERVER['FLOW_WEBPATH']) && is_dir($_SERVER['FLOW_WEBPATH'])) { - define('FLOW_PATH_WEB', Files::getUnixStylePath(realpath($_SERVER['FLOW_WEBPATH'])) . '/'); + $webPath = realpath($_SERVER['FLOW_WEBPATH']); + if ($webPath === false) { + throw new \RuntimeException('Failed to resolve web path', 1744459038); + } + define('FLOW_PATH_WEB', Files::getUnixStylePath($webPath) . '/'); } else { define('FLOW_PATH_WEB', FLOW_PATH_ROOT . 'Web/'); } } } else { if (!defined('FLOW_PATH_ROOT')) { - define('FLOW_PATH_ROOT', Files::getUnixStylePath(realpath(dirname($_SERVER['SCRIPT_FILENAME']) . '/../')) . '/'); + $rootPath = realpath(dirname($_SERVER['SCRIPT_FILENAME']) . '/../'); + if ($rootPath === false) { + throw new \RuntimeException('Failed to resolve root path', 1744459011); + } + define('FLOW_PATH_ROOT', Files::getUnixStylePath($rootPath) . '/'); + } + $webPath = realpath(dirname($_SERVER['SCRIPT_FILENAME'])); + if ($webPath === false) { + throw new \RuntimeException('Failed to resolve web path', 1744459038); } - define('FLOW_PATH_WEB', Files::getUnixStylePath(realpath(dirname($_SERVER['SCRIPT_FILENAME']))) . '/'); + define('FLOW_PATH_WEB', Files::getUnixStylePath($webPath) . '/'); } define('FLOW_PATH_CONFIGURATION', FLOW_PATH_ROOT . 'Configuration/'); diff --git a/Neos.Flow/Classes/Core/ClassLoader.php b/Neos.Flow/Classes/Core/ClassLoader.php index c936b2b759..f827a21591 100644 --- a/Neos.Flow/Classes/Core/ClassLoader.php +++ b/Neos.Flow/Classes/Core/ClassLoader.php @@ -12,7 +12,7 @@ */ use Neos\Flow\Annotations as Flow; -use Neos\Flow\Package; +use Neos\Flow\Package\PackageInterface; use Neos\Utility\Files; /** @@ -46,7 +46,7 @@ class ClassLoader /** * A list of namespaces this class loader is definitely responsible for. * - * @var array + * @var array */ protected $packageNamespaces = []; @@ -56,7 +56,7 @@ class ClassLoader protected $considerTestsNamespace = false; /** - * @var array + * @var array */ protected $ignoredClassNames = [ 'integer' => true, @@ -78,7 +78,7 @@ class ClassLoader ]; /** - * @var array + * @var array */ protected $fallbackClassPaths = []; @@ -87,13 +87,13 @@ class ClassLoader * to save time in resolving those non existent classes. * Usually these will be annotations that have no class. * - * @var array + * @var array * */ protected $nonExistentClasses = []; /** - * @param array $defaultPackageEntries Adds default entries for packages that should be available for very early loading + * @param array $defaultPackageEntries Adds default entries for packages that should be available for very early loading */ public function __construct(array $defaultPackageEntries = []) { @@ -161,8 +161,8 @@ public function loadClass(string $className): bool * there may exist a package "Neos" and a package "Neos.NodeTypes" -- so a class Neos\NodeTypes\Foo must be first * loaded (if it exists) from Neos.NodeTypes, falling back to Neos afterwards. * - * @param array $possiblePaths - * @param array $namespaceParts + * @param array $possiblePaths + * @param array $namespaceParts * @param integer $packageNamespacePartCount * @return boolean */ @@ -191,13 +191,13 @@ protected function loadClassFromPossiblePaths(array $possiblePaths, array $names /** * Sets the available packages * - * @param array $activePackages An array of \Neos\Flow\Package\Package objects + * @param array $activePackages * @return void */ public function setPackages(array $activePackages) { - /** @var Package $package */ - foreach ($activePackages as $packageKey => $package) { + foreach ($activePackages as $package) { + /** @phpstan-ignore method.notFound (not part of the interface) */ foreach ($package->getFlattenedAutoloadConfiguration() as $configuration) { $this->createNamespaceMapEntry($configuration['namespace'], $configuration['classPath'], $configuration['mappingType']); } @@ -292,7 +292,7 @@ protected function removeNamespaceMapEntry(string $namespace, string $classPath, /** * Try to build a path to a class according to PSR-0 rules. * - * @param array $classNameParts Parts of the FQ classname. + * @param array $classNameParts Parts of the FQ classname. * @param string $classPath Already detected class path to a possible package. * @return string */ @@ -306,7 +306,7 @@ protected function buildClassPathWithPsr0(array $classNameParts, string $classPa /** * Try to build a path to a class according to PSR-4 rules. * - * @param array $classNameParts Parts of the FQ classname. + * @param array $classNameParts Parts of the FQ classname. * @param string $classPath Already detected class path to a possible package. * @param integer $packageNamespacePartCount Amount of parts of the className that is also part of the package namespace. * @return string diff --git a/Neos.Flow/Classes/Core/LockManager.php b/Neos.Flow/Classes/Core/LockManager.php index 26d228fbb5..dce2fd830d 100644 --- a/Neos.Flow/Classes/Core/LockManager.php +++ b/Neos.Flow/Classes/Core/LockManager.php @@ -47,7 +47,7 @@ class LockManager protected $lockHoldingPage; /** - * @var resource + * @var resource|null */ protected $lockResource; @@ -128,8 +128,8 @@ public function exitIfSiteLocked() public function lockSiteOrExit() { touch($this->lockFlagPathAndFilename); - $this->lockResource = fopen($this->lockPathAndFilename, 'w+'); - if (!flock($this->lockResource, LOCK_EX | LOCK_NB)) { + $this->lockResource = fopen($this->lockPathAndFilename, 'w+') ?: null; + if ($this->lockResource && !flock($this->lockResource, LOCK_EX | LOCK_NB)) { fclose($this->lockResource); $this->doExit(); } @@ -168,6 +168,7 @@ public function unlockSite() */ protected function doExit() { + /** @phpstan-ignore identical.alwaysFalse (can also be "Web") */ if (FLOW_SAPITYPE === 'Web') { header('HTTP/1.1 503 Service Temporarily Unavailable'); readfile($this->lockHoldingPage); diff --git a/Neos.Flow/Classes/Core/ProxyClassLoader.php b/Neos.Flow/Classes/Core/ProxyClassLoader.php index 350b642ba9..9cf114c8e0 100644 --- a/Neos.Flow/Classes/Core/ProxyClassLoader.php +++ b/Neos.Flow/Classes/Core/ProxyClassLoader.php @@ -25,7 +25,7 @@ class ProxyClassLoader { /** - * @var array + * @var array */ protected $ignoredClassNames = [ 'integer' => true, @@ -52,7 +52,7 @@ class ProxyClassLoader protected $classesCache; /** - * @var array + * @var ?array */ protected $availableProxyClasses; diff --git a/Neos.Flow/Classes/Error/AbstractExceptionHandler.php b/Neos.Flow/Classes/Error/AbstractExceptionHandler.php index 1e753eb284..7c586361d2 100644 --- a/Neos.Flow/Classes/Error/AbstractExceptionHandler.php +++ b/Neos.Flow/Classes/Error/AbstractExceptionHandler.php @@ -32,17 +32,17 @@ abstract class AbstractExceptionHandler implements ExceptionHandlerInterface { /** - * @var LoggerInterface + * @var ?LoggerInterface */ protected $logger; /** - * @var ThrowableStorageInterface + * @var ?ThrowableStorageInterface */ protected $throwableStorage; /** - * @var array + * @var array */ protected $options = []; @@ -51,11 +51,11 @@ abstract class AbstractExceptionHandler implements ExceptionHandlerInterface * * @var array{ * viewClassName: string, - * viewOptions: array, + * viewOptions: array, * renderTechnicalDetails: bool, - * logException: bool, + * logException?: bool, * renderingGroup?: string, - * variables?: array + * variables?: array * } */ protected $renderingOptions; @@ -65,7 +65,7 @@ abstract class AbstractExceptionHandler implements ExceptionHandlerInterface * @return void * @Flow\Autowiring(enabled=false) */ - public function injectLogger(LoggerInterface $logger) + public function injectLogger(LoggerInterface $logger): void { $this->logger = $logger; } @@ -73,7 +73,7 @@ public function injectLogger(LoggerInterface $logger) /** * @param ThrowableStorageInterface $throwableStorage */ - public function injectThrowableStorage(ThrowableStorageInterface $throwableStorage) + public function injectThrowableStorage(ThrowableStorageInterface $throwableStorage): void { $this->throwableStorage = $throwableStorage; } @@ -81,7 +81,7 @@ public function injectThrowableStorage(ThrowableStorageInterface $throwableStora /** * Sets options of this exception handler. * - * @param array $options Options for the exception handler + * @param array $options Options for the exception handler * @return void */ public function setOptions(array $options) @@ -117,7 +117,7 @@ public function handleException($exception) $exceptionWasLogged = false; if ($this->throwableStorage instanceof ThrowableStorageInterface && isset($this->renderingOptions['logException']) && $this->renderingOptions['logException']) { $message = $this->throwableStorage->logThrowable($exception); - $this->logger->critical($message); + $this->logger?->critical($message); $exceptionWasLogged = true; } @@ -141,7 +141,7 @@ abstract protected function echoExceptionWeb($exception); * Prepares a view for rendering the custom error page. * * @param \Throwable $exception - * @param array $renderingOptions Rendering options as defined in the settings + * @param array $renderingOptions Rendering options as defined in the settings * @return ViewInterface */ protected function buildView(\Throwable $exception, array $renderingOptions): ViewInterface @@ -190,7 +190,14 @@ protected function buildView(\Throwable $exception, array $renderingOptions): Vi * Checks if custom rendering rules apply to the given $exception and returns those. * * @param \Throwable $exception - * @return array the custom rendering options, or the default + * @return array{ + * viewClassName: string, + * viewOptions: array, + * renderTechnicalDetails: bool, + * logException?: bool, + * renderingGroup?: string, + * variables?: array + * } the custom rendering options, or the default */ protected function resolveCustomRenderingOptions(\Throwable $exception): array { @@ -309,7 +316,7 @@ protected function renderSingleExceptionCli(\Throwable $exception): string $exceptionMessage .= $this->renderExceptionDetailCli('Code', $exception->getCode()); } $exceptionMessage .= $this->renderExceptionDetailCli('File', str_replace(FLOW_PATH_ROOT, '', $exception->getFile())); - $exceptionMessage .= $this->renderExceptionDetailCli('Line', $exception->getLine()); + $exceptionMessage .= $this->renderExceptionDetailCli('Line', (string)$exception->getLine()); return $exceptionMessage; } @@ -336,7 +343,7 @@ protected function renderExceptionDetailCli(string $label, string $value): strin * - Otherwise the subject is everything until the first line break or end of sentence, the body contains the rest * * @param string $exceptionMessage - * @return array in the format array('subject' => '', 'body' => ''); + * @return array{subject: string, body: string} */ protected function splitExceptionMessage(string $exceptionMessage): array { diff --git a/Neos.Flow/Classes/Error/Debugger.php b/Neos.Flow/Classes/Error/Debugger.php index de53c73e42..a0aadef08a 100644 --- a/Neos.Flow/Classes/Error/Debugger.php +++ b/Neos.Flow/Classes/Error/Debugger.php @@ -27,13 +27,13 @@ class Debugger { /** - * @var ObjectManagerInterface + * @var ?ObjectManagerInterface */ protected static $objectManager; /** * - * @var array + * @var array */ protected static $renderedObjects = []; @@ -41,7 +41,7 @@ class Debugger * Hardcoded list of Flow class names (regex) which should not be displayed during debugging. * This is a fallback in case the classes could not be fetched from the configuration * - * @var array + * @var array */ protected static $ignoredClassesFallback = [ 'Neos\\\\Flow\\\\Aop.*' => true, @@ -138,7 +138,7 @@ public static function renderDump($variable, int $level, bool $plaintext = false $dump = sprintf('\'%s\' (%s)', htmlspecialchars($croppedValue), strlen($variable)); } } elseif (is_numeric($variable)) { - $dump = sprintf('%s %s', gettype($variable), self::ansiEscapeWrap($variable, '35', $ansiColors)); + $dump = sprintf('%s %s', gettype($variable), self::ansiEscapeWrap((string)$variable, '35', $ansiColors)); } elseif (is_iterable($variable)) { $dump = self::renderArrayDump($variable, $level + 1, $plaintext, $ansiColors); } elseif (is_object($variable)) { @@ -157,7 +157,7 @@ public static function renderDump($variable, int $level, bool $plaintext = false /** * Renders a dump of the given array * - * @param iterable $array + * @param iterable $array * @param integer $level * @param boolean $plaintext * @param boolean $ansiColors @@ -320,7 +320,7 @@ protected static function renderObjectDump($object, int $level, bool $renderProp /** * Renders some backtrace * - * @param array $trace The trace + * @param array> $trace The trace * @param boolean $includeCode Include code snippet * @param boolean $plaintext * @return string Backtrace information @@ -393,7 +393,7 @@ protected static function renderCallArgumentsHtml(array $callArguments): string } /** - * @param array $trace + * @param array> $trace * @param bool $includeCode * @return string */ @@ -547,7 +547,7 @@ protected static function getCodeSnippetPlaintext(string $filePathAndName, int $ /** * @param string $file - * @return array + * @return array{short: string, proxy: string} */ public static function findProxyAndShortFilePath(string $file): array { @@ -556,7 +556,9 @@ public static function findProxyAndShortFilePath(string $file): array $proxyClassPathPosition = strpos($file, 'Flow_Object_Classes/'); if ($proxyClassPathPosition && is_file($file)) { $fileContent = @file($file); - $originalPath = trim(substr($fileContent[count($fileContent) - 2], 19)); + if ($fileContent !== false) { + $originalPath = trim(substr($fileContent[count($fileContent) - 2], 19)); + } } $originalPath = str_replace($flowRoot, '', $originalPath); @@ -603,11 +605,9 @@ public static function getIgnoredClassesRegex(): string if (self::$objectManager instanceof ObjectManagerInterface) { $configurationManager = self::$objectManager->get(ConfigurationManager::class); - if ($configurationManager instanceof ConfigurationManager) { - $ignoredClassesFromSettings = $configurationManager->getConfiguration('Settings', 'Neos.Flow.error.debugger.ignoredClasses'); - if (is_array($ignoredClassesFromSettings)) { - $ignoredClassesConfiguration = Arrays::arrayMergeRecursiveOverrule($ignoredClassesConfiguration, $ignoredClassesFromSettings); - } + $ignoredClassesFromSettings = $configurationManager->getConfiguration('Settings', 'Neos.Flow.error.debugger.ignoredClasses'); + if (is_array($ignoredClassesFromSettings)) { + $ignoredClassesConfiguration = Arrays::arrayMergeRecursiveOverrule($ignoredClassesConfiguration, $ignoredClassesFromSettings); } } @@ -639,11 +639,9 @@ public static function getRecursionLimit(): int if (self::$objectManager instanceof ObjectManagerInterface) { $configurationManager = self::$objectManager->get(ConfigurationManager::class); - if ($configurationManager instanceof ConfigurationManager) { - $recursionLimitFromSettings = $configurationManager->getConfiguration('Settings', 'Neos.Flow.error.debugger.recursionLimit'); - if (is_int($recursionLimitFromSettings)) { - self::$recursionLimit = $recursionLimitFromSettings; - } + $recursionLimitFromSettings = $configurationManager->getConfiguration('Settings', 'Neos.Flow.error.debugger.recursionLimit'); + if (is_int($recursionLimitFromSettings)) { + self::$recursionLimit = $recursionLimitFromSettings; } } @@ -668,7 +666,9 @@ public static function getRecursionLimit(): int function var_dump($variable, ?string $title = null, bool $return = false, ?bool $plaintext = null) { if ($plaintext === null) { + /** @phpstan-ignore identical.alwaysTrue (FLOW_SAPITYPE can also be "Web") */ $plaintext = (FLOW_SAPITYPE === 'CLI'); + /** @phpstan-ignore booleanAnd.leftAlwaysTrue (no, this can be false) */ $ansiColors = $plaintext && DIRECTORY_SEPARATOR === '/'; } else { $ansiColors = false; diff --git a/Neos.Flow/Classes/Error/ErrorHandler.php b/Neos.Flow/Classes/Error/ErrorHandler.php index 1bb8b32eb1..fb7005f582 100644 --- a/Neos.Flow/Classes/Error/ErrorHandler.php +++ b/Neos.Flow/Classes/Error/ErrorHandler.php @@ -22,7 +22,7 @@ class ErrorHandler { /** - * @var array + * @var array */ protected $exceptionalErrors = []; @@ -32,13 +32,15 @@ class ErrorHandler */ public function __construct() { - set_error_handler([$this, 'handleError']); + /** @var callable $callback */ + $callback = [$this, 'handleError']; + set_error_handler($callback); } /** * Defines which error levels result should result in an exception thrown. * - * @param array $exceptionalErrors An array of E_* error levels + * @param array $exceptionalErrors An array of E_* error levels * @return void */ public function setExceptionalErrors(array $exceptionalErrors) diff --git a/Neos.Flow/Classes/Error/ExceptionHandlerInterface.php b/Neos.Flow/Classes/Error/ExceptionHandlerInterface.php index ce8faeff9c..f1d1005b1c 100644 --- a/Neos.Flow/Classes/Error/ExceptionHandlerInterface.php +++ b/Neos.Flow/Classes/Error/ExceptionHandlerInterface.php @@ -27,7 +27,7 @@ public function handleException($exception); /** * Sets options of this exception handler * - * @param array $options + * @param array $options * @return void */ public function setOptions(array $options); diff --git a/Neos.Flow/Classes/Error/ProductionExceptionHandler.php b/Neos.Flow/Classes/Error/ProductionExceptionHandler.php index a637f90e0e..a629a31997 100644 --- a/Neos.Flow/Classes/Error/ProductionExceptionHandler.php +++ b/Neos.Flow/Classes/Error/ProductionExceptionHandler.php @@ -56,8 +56,10 @@ protected function echoExceptionWeb($exception) echo $this->renderStatically($statusCode, $referenceCode); } } catch (\Exception $innerException) { - $message = $this->throwableStorage->logThrowable($innerException); - $this->logger->critical($message); + $message = $this->throwableStorage?->logThrowable($innerException); + if ($message) { + $this->logger?->critical($message); + } } } diff --git a/Neos.Flow/Classes/Exception.php b/Neos.Flow/Classes/Exception.php index 0029184a32..7cdfc97152 100644 --- a/Neos.Flow/Classes/Exception.php +++ b/Neos.Flow/Classes/Exception.php @@ -22,7 +22,7 @@ class Exception extends \Exception implements WithReferenceCodeInterface, WithHttpStatusInterface { /** - * @var string + * @var ?string */ protected $referenceCode; @@ -41,7 +41,7 @@ class Exception extends \Exception implements WithReferenceCodeInterface, WithHt public function getReferenceCode() { if (!isset($this->referenceCode)) { - $this->referenceCode = date('YmdHis', $_SERVER['REQUEST_TIME']) . substr(md5(rand()), 0, 6); + $this->referenceCode = date('YmdHis', $_SERVER['REQUEST_TIME']) . substr(md5((string)rand()), 0, 6); } return $this->referenceCode; } diff --git a/Neos.Flow/Classes/Http/CacheControlDirectives.php b/Neos.Flow/Classes/Http/CacheControlDirectives.php index 4c6bda2bd3..f3fefe8544 100644 --- a/Neos.Flow/Classes/Http/CacheControlDirectives.php +++ b/Neos.Flow/Classes/Http/CacheControlDirectives.php @@ -19,7 +19,7 @@ class CacheControlDirectives { /** - * @var array + * @var array */ protected $cacheDirectives = [ 'visibility' => '', @@ -170,7 +170,7 @@ public function getDirective(string $name) } /** - * @return array + * @return array */ public function getDirectives(): array { diff --git a/Neos.Flow/Classes/Http/Client/Browser.php b/Neos.Flow/Classes/Http/Client/Browser.php index 50c1112480..f71b854c20 100644 --- a/Neos.Flow/Classes/Http/Client/Browser.php +++ b/Neos.Flow/Classes/Http/Client/Browser.php @@ -62,7 +62,7 @@ class Browser implements ClientInterface * A simple string array that keeps track of occurred "Location" header * redirections to avoid infinite loops if the same redirection happens * - * @var array + * @var array */ protected $redirectionStack = []; @@ -112,7 +112,7 @@ public function setRequestEngine(RequestEngineInterface $requestEngine) * Allows to add headers to be sent with every request the browser executes. * * @param string $name Name of the header, for example "Location", "Content-Description" etc. - * @param array|string|\DateTime $values An array of values or a single value for the specified header field + * @param array|string|\DateTime|\DateTimeImmutable $values An array of values or a single value for the specified header field * @return void * @see Message::setHeader() */ @@ -139,23 +139,20 @@ public function removeAutomaticRequestHeader($name) * * @param string|UriInterface $uri * @param string $method Request method, for example "GET" - * @param array $arguments Arguments to send in the request body + * @param array $arguments Arguments to send in the request body * @param UploadedFileInterface[]|mixed[][] $files A (deep) array of UploadedFile or an untangled $_FILES array - * @param array $server + * @param array $server * @param string $content * @return ResponseInterface The HTTP response * @throws \InvalidArgumentException * @throws InfiniteRedirectionException * @api */ - public function request($uri, $method = 'GET', array $arguments = [], array $files = [], array $server = [], $content = null): ResponseInterface + public function request(string|UriInterface $uri, $method = 'GET', array $arguments = [], array $files = [], array $server = [], $content = null): ResponseInterface { if (is_string($uri)) { $uri = new Uri($uri); } - if (!$uri instanceof UriInterface) { - throw new \InvalidArgumentException('$uri must be a URI object or a valid string representation of a URI.', 1333443624); - } $request = $this->serverRequestFactory->createServerRequest($method, $uri, $server); if ($content) { $request = $request->withBody($this->contentStreamFactory->createStream($content)); diff --git a/Neos.Flow/Classes/Http/Client/CurlEngine.php b/Neos.Flow/Classes/Http/Client/CurlEngine.php index f831902812..5b82b3903b 100644 --- a/Neos.Flow/Classes/Http/Client/CurlEngine.php +++ b/Neos.Flow/Classes/Http/Client/CurlEngine.php @@ -25,7 +25,7 @@ class CurlEngine implements RequestEngineInterface { /** - * @var array + * @var array */ protected $options = [ CURLOPT_RETURNTRANSFER => true, @@ -42,9 +42,9 @@ class CurlEngine implements RequestEngineInterface * * @param integer $optionName One of the CURLOPT_* constants * @param mixed $value The value to set - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ - public function setOption($optionName, $value) + public function setOption($optionName, $value): void { if ($optionName === CURLOPT_HTTPHEADER) { throw new InvalidArgumentException("Setting CURL headers is only possible via the request object and not by using the setOption method.", 1633334307); @@ -95,6 +95,9 @@ public function sendRequest(RequestInterface $request): ResponseInterface curl_setopt($curlHandle, CURLOPT_PUT, true); if ($content !== '') { $inFileHandler = fopen('php://temp', 'r+'); + if ($inFileHandler === false) { + throw new \RuntimeException('Failed to open php://temp', 1744451653); + } fwrite($inFileHandler, $content); rewind($inFileHandler); curl_setopt_array($curlHandle, [ @@ -111,7 +114,8 @@ public function sendRequest(RequestInterface $request): ResponseInterface default: $body = $content !== '' ? $content : $request->getUri()->getQuery(); curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body); - curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, $request->getMethod()); + $method = $request->getMethod(); + curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, $method === '' ? null : $method); } $preparedHeaders = []; @@ -134,7 +138,7 @@ public function sendRequest(RequestInterface $request): ResponseInterface } $curlResult = curl_exec($curlHandle); - if ($curlResult === false) { + if (!is_string($curlResult)) { throw new CurlEngineException(sprintf('cURL reported error code %s with message "%s". Last requested URL was "%s" (%s).', curl_errno($curlHandle), curl_error($curlHandle), curl_getinfo($curlHandle, CURLINFO_EFFECTIVE_URL), $request->getMethod()), 1338906040); } diff --git a/Neos.Flow/Classes/Http/Client/InternalRequestEngine.php b/Neos.Flow/Classes/Http/Client/InternalRequestEngine.php index efe9b87913..65e5260214 100644 --- a/Neos.Flow/Classes/Http/Client/InternalRequestEngine.php +++ b/Neos.Flow/Classes/Http/Client/InternalRequestEngine.php @@ -174,7 +174,7 @@ public function getRouter(): RouterInterface /** * Prepare a response in case an error occurred. * - * @param object $exception \Exception or \Throwable + * @param \Throwable $exception * @param ResponseInterface $response * @return ResponseInterface */ @@ -197,7 +197,7 @@ protected function prepareErrorResponse($exception, ResponseInterface $response) return $response ->withStatus($statusCode) ->withBody($this->contentFactory->createStream($content)) - ->withHeader('X-Flow-ExceptionCode', $exception->getCode()) + ->withHeader('X-Flow-ExceptionCode', (string)$exception->getCode()) ->withHeader('X-Flow-ExceptionMessage', base64_encode($exception->getMessage())); } } diff --git a/Neos.Flow/Classes/Http/ContentStream.php b/Neos.Flow/Classes/Http/ContentStream.php index b502c8da61..3e6d229e76 100644 --- a/Neos.Flow/Classes/Http/ContentStream.php +++ b/Neos.Flow/Classes/Http/ContentStream.php @@ -59,6 +59,9 @@ public function __construct($stream, $mode = 'r') public static function fromContents(string $contents): self { $handle = fopen('php://memory', 'r+'); + if ($handle === false) { + throw new \RuntimeException('Failed to open php://memory', 1744451485); + } fwrite($handle, $contents); rewind($handle); return new static($handle); @@ -71,12 +74,14 @@ public static function fromContents(string $contents): self */ public function close() { - if (!$this->resource) { + if ($this->resource === null) { return; } $resource = $this->detach(); - fclose($resource); + if ($resource !== null) { + fclose($resource); + } } /** @@ -100,10 +105,10 @@ public function detach() * @param string|resource $stream * @param string $mode */ - public function replace($stream, $mode = 'r') + public function replace(mixed $stream, $mode = 'r'): void { $this->close(); - if (!is_resource($stream)) { + if (is_string($stream)) { $stream = @fopen($stream, $mode); } @@ -127,6 +132,7 @@ public function getSize() $stats = fstat($this->resource); + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (size is available in fstat) */ return $stats['size']; } @@ -218,7 +224,7 @@ public function seek($offset, $whence = SEEK_SET) * @link http://www.php.net/manual/en/function.fseek.php * @throws \RuntimeException on failure. */ - public function rewind() + public function rewind(): bool { return $this->seek(0); } @@ -227,6 +233,7 @@ public function rewind() * Returns whether or not the stream is writable. * * @return bool + * @phpstan-assert-if-true resource $this->resource */ public function isWritable() { @@ -272,6 +279,7 @@ public function write($string) * Returns whether or not the stream is readable. * * @return bool + * @phpstan-assert-if-true resource $this->resource */ public function isReadable() { @@ -288,14 +296,14 @@ public function isReadable() /** * Read data from the stream. * - * @param int $length Read up to $length bytes from the object and return + * @param int<1, max> $length Read up to $length bytes from the object and return * them. Fewer than $length bytes may be returned if underlying stream * call returns fewer bytes. * @return string Returns the data read from the stream, or an empty string * if no bytes are available. * @throws \RuntimeException if an error occurs. */ - public function read($length) + public function read(int $length) { $this->ensureResourceReadable(); @@ -341,6 +349,9 @@ public function getContents() */ public function getMetadata($key = null) { + if (!$this->resource) { + return null; + } if ($key === null) { return stream_get_meta_data($this->resource); } @@ -355,8 +366,9 @@ public function getMetadata($key = null) /** * Throw an exception if the current resource is not readable. + * @phpstan-assert resource $this->resource */ - protected function ensureResourceReadable() + protected function ensureResourceReadable(): void { if ($this->isReadable() === false) { throw new \RuntimeException('Stream is not readable.', 1453892039); @@ -365,8 +377,9 @@ protected function ensureResourceReadable() /** * Throw an exception if the current resource is not valid. + * @phpstan-assert resource $this->resource */ - protected function ensureResourceOpen() + protected function ensureResourceOpen(): void { if (!$this->isValidResource($this->resource)) { throw new \RuntimeException('No resource available to apply operation', 1453891806); @@ -374,7 +387,9 @@ protected function ensureResourceOpen() } /** + * @param mixed $resource * @return boolean + * @phpstan-assert-if-true resource $resource */ protected function isValidResource($resource) { @@ -406,9 +421,7 @@ public function __toString() return $this->getContents(); } catch (\Exception $e) { - if ($this->logger instanceof LoggerInterface) { - $this->logger->error(sprintf('Tried to convert a http content stream to a string but an exception occured: [%s] - %s', $e->getCode(), $e->getMessage()), ['exception' => $e] + LogEnvironment::fromMethodName(__METHOD__)); - } + $this->logger->error(sprintf('Tried to convert a http content stream to a string but an exception occured: [%s] - %s', $e->getCode(), $e->getMessage()), ['exception' => $e] + LogEnvironment::fromMethodName(__METHOD__)); return ''; } } diff --git a/Neos.Flow/Classes/Http/Cookie.php b/Neos.Flow/Classes/Http/Cookie.php index d1a06e59f0..8bd3882779 100644 --- a/Neos.Flow/Classes/Http/Cookie.php +++ b/Neos.Flow/Classes/Http/Cookie.php @@ -54,7 +54,7 @@ class Cookie protected $name; /** - * @var string + * @var mixed */ protected $value; @@ -66,18 +66,18 @@ class Cookie /** * Number of seconds until the cookie expires (RFC 6265, 4.1.2.2) - * @var integer + * @var ?integer */ protected $maximumAge; /** * Hosts to which this cookie will be sent (RFC 6265, 4.1.2.3) - * @var string + * @var ?string */ protected $domain; /** - * @var string + * @var ?string */ protected $path; @@ -113,30 +113,24 @@ class Cookie * * @param string $name The cookie name as a valid token (RFC 2616) * @param mixed $value The value to store in the cookie. Must be possible to cast into a string. - * @param integer|\DateTime $expires Date and time after which this cookie expires. - * @param integer $maximumAge Number of seconds until the cookie expires. - * @param string $domain The host to which the user agent will send this cookie - * @param string $path The path describing the scope of this cookie + * @param integer|\DateTimeInterface $expires Date and time after which this cookie expires. + * @param ?integer $maximumAge Number of seconds until the cookie expires. + * @param ?string $domain The host to which the user agent will send this cookie + * @param ?string $path The path describing the scope of this cookie * @param boolean $secure If this cookie should only be sent through a "secure" channel by the user agent * @param boolean $httpOnly If this cookie should only be used through the HTTP protocol * @param string $sameSite If this cookie should restricted to a first-party or top-level navigation or third-party context * @api * @throws \InvalidArgumentException */ - public function __construct($name, $value = null, $expires = 0, $maximumAge = null, $domain = null, $path = '/', $secure = false, $httpOnly = true, $sameSite = null) + public function __construct($name, $value = null, int|\DateTimeInterface $expires = 0, ?int $maximumAge = null, $domain = null, ?string $path = '/', $secure = false, $httpOnly = true, $sameSite = null) { if (preg_match(self::PATTERN_TOKEN, $name) !== 1) { throw new \InvalidArgumentException('The parameter "name" passed to the Cookie constructor must be a valid token as per RFC 2616, Section 2.2.', 1345101977); } - if ($expires instanceof \Datetime) { + if ($expires instanceof \DateTimeInterface) { $expires = $expires->getTimestamp(); } - if (!is_int($expires)) { - throw new \InvalidArgumentException('The parameter "expires" passed to the Cookie constructor must be a unix timestamp or a DateTime object.', 1345108785); - } - if ($maximumAge !== null && !is_int($maximumAge)) { - throw new \InvalidArgumentException('The parameter "maximumAge" passed to the Cookie constructor must be an integer value.', 1345108786); - } if ($domain !== null && preg_match(self::PATTERN_DOMAIN, $domain) !== 1) { throw new \InvalidArgumentException('The parameter "domain" passed to the Cookie constructor must be a valid domain as per RFC 6265, Section 4.1.2.3.', 1345116246); } @@ -329,7 +323,7 @@ public function getMaximumAge() /** * Returns the domain this cookie is valid for. * - * @return string The domain name + * @return ?string The domain name * @api */ public function getDomain() @@ -340,7 +334,7 @@ public function getDomain() /** * Returns the path this cookie is valid for. * - * @return string The path + * @return ?string The path * @api */ public function getPath() diff --git a/Neos.Flow/Classes/Http/Headers.php b/Neos.Flow/Classes/Http/Headers.php index 6f12190574..661cce0c41 100644 --- a/Neos.Flow/Classes/Http/Headers.php +++ b/Neos.Flow/Classes/Http/Headers.php @@ -18,17 +18,18 @@ * * @deprecated Headers will be only accessed via request in the future, if this class stays, then as internal implementation detail. * @Flow\Proxy(false) + * @implements \Iterator * TODO: case-insensitive header name matching */ class Headers implements \Iterator { /** - * @var array + * @var array */ protected array $fields = ['Cache-Control' => []]; /** - * @var array + * @var array */ protected array $cookies = []; @@ -40,7 +41,7 @@ class Headers implements \Iterator /** * Constructs a new Headers object. * - * @param array $fields Field names and their values (either as single value or array of values) + * @param array $fields Field names and their values (either as single value or array of values) */ public function __construct(array $fields = []) { @@ -53,7 +54,7 @@ public function __construct(array $fields = []) /** * Creates a new Headers instance from the given $_SERVER-superglobal-like array. * - * @param array $server An array similar or equal to $_SERVER, containing headers in the form of "HTTP_FOO_BAR" + * @param array $server An array similar or equal to $_SERVER, containing headers in the form of "HTTP_FOO_BAR" * @return Headers */ public static function createFromServer(array $server) @@ -88,7 +89,7 @@ public static function createFromServer(array $server) * GMT previously. GMT is used synonymously with UTC as per RFC 2616 3.3.1. * * @param string $name Name of the header, for example "Location", "Content-Description" etc. - * @param array|string|\DateTime $values An array of values or a single value for the specified header field + * @param array|string|\DateTime|\DateTimeImmutable $values An array of values or a single value for the specified header field * @param boolean $replaceExistingHeader If a header with the same name should be replaced. Default is true. * @return void * @throws \InvalidArgumentException @@ -96,10 +97,13 @@ public static function createFromServer(array $server) */ public function set($name, $values, $replaceExistingHeader = true) { - if ($values instanceof \DateTimeInterface) { + if ($values instanceof \DateTime) { $date = clone $values; $date->setTimezone(new \DateTimeZone('GMT')); $values = [$date->format('D, d M Y H:i:s') . ' GMT']; + } elseif ($values instanceof \DateTimeImmutable) { + $date = $values->setTimezone(new \DateTimeZone('GMT')); + $values = [$date->format('D, d M Y H:i:s') . ' GMT']; } else { $values = (array) $values; } @@ -161,7 +165,7 @@ public function getRaw(string $name): array * Dates are returned as DateTime objects with the timezone set to GMT. * * @param string $name Name of the header, for example "Location", "Content-Description" etc. - * @return array|string|null An array of field values if multiple headers of that name exist, a string value if only one value exists and NULL if there is no such header. + * @return array|string|null An array of field values if multiple headers of that name exist, a string value if only one value exists and NULL if there is no such header. * @api */ public function get($name) @@ -200,7 +204,7 @@ public function get($name) * Note that even for those header fields which exist only one time, the value is * returned as an array (with a single item). * - * @return array + * @return array * @api */ public function getAll() @@ -384,7 +388,7 @@ protected function setCookiesFromRawHeader($rawFieldValue) /** * Get all header lines prepared as "name: value" strings. * - * @return array + * @return array */ public function getPreparedValues() { @@ -398,8 +402,8 @@ public function getPreparedValues() /** * @param string $headerName - * @param array $values - * @return array + * @param array $values + * @return array */ private function prepareValues($headerName, array $values) { @@ -413,7 +417,7 @@ private function prepareValues($headerName, array $values) /** * @param string $headerName - * @param array $values + * @param array $values * @return string */ private function renderValuesFor($headerName, array $values) diff --git a/Neos.Flow/Classes/Http/Helper/ArgumentsHelper.php b/Neos.Flow/Classes/Http/Helper/ArgumentsHelper.php index 331c146c24..01df53c157 100644 --- a/Neos.Flow/Classes/Http/Helper/ArgumentsHelper.php +++ b/Neos.Flow/Classes/Http/Helper/ArgumentsHelper.php @@ -25,10 +25,10 @@ abstract class ArgumentsHelper * * The order of merging is GET, POST, FILES, with later items overriding earlier ones. * - * @param array $getArguments Arguments as found in $_GET - * @param array $postArguments Arguments as found in $_POST - * @param array $untangledFiles Untangled $_FILES as provided by \Neos\Flow\Http\Helper\UploadedFilesHelper::untangleFilesArray - * @return array the unified arguments + * @param array $getArguments Arguments as found in $_GET + * @param array $postArguments Arguments as found in $_POST + * @param array $untangledFiles Untangled $_FILES as provided by \Neos\Flow\Http\Helper\UploadedFilesHelper::untangleFilesArray + * @return array the unified arguments * @see \Neos\Flow\Http\Helper\UploadedFilesHelper::untangleFilesArray */ public static function buildUnifiedArguments(array $getArguments, array $postArguments, array $untangledFiles): array @@ -40,8 +40,9 @@ public static function buildUnifiedArguments(array $getArguments, array $postArg } /** - * @param array ...$argumentArrays - * @return array + * @param array $arguments + * @param array ...$argumentArrays + * @return array */ public static function mergeArgumentArrays(array $arguments, array ...$argumentArrays): array { diff --git a/Neos.Flow/Classes/Http/Helper/MediaTypeHelper.php b/Neos.Flow/Classes/Http/Helper/MediaTypeHelper.php index 67cbf296e8..9eb5b5d50d 100644 --- a/Neos.Flow/Classes/Http/Helper/MediaTypeHelper.php +++ b/Neos.Flow/Classes/Http/Helper/MediaTypeHelper.php @@ -24,12 +24,12 @@ abstract class MediaTypeHelper * If no "Accept" header was found all media types are acceptable. * * @param RequestInterface $request - * @return array + * @return array */ public static function determineAcceptedMediaTypes(RequestInterface $request): array { $rawValues = $request->getHeaderLine('Accept'); - if (empty($rawValues) || !is_string($rawValues)) { + if (empty($rawValues)) { return ['*/*']; } $acceptedMediaTypes = self::parseContentNegotiationQualityValues($rawValues); @@ -41,8 +41,8 @@ public static function determineAcceptedMediaTypes(RequestInterface $request): a * Returns the best fitting IANA media type after applying the content negotiation * rules on the accepted media types. * - * @param array $acceptedMediaTypes A list of accepted media types according to a request. - * @param array $supportedMediaTypes A list of media types which are supported by the application / controller + * @param array $acceptedMediaTypes A list of accepted media types according to a request. + * @param array $supportedMediaTypes A list of media types which are supported by the application / controller * @param bool $trim If TRUE, only the type/subtype of the media type is returned. If FALSE, the full original media type string is returned. * @return string The media type and sub type which matched, NULL if none matched */ @@ -66,17 +66,20 @@ public static function negotiateMediaType(array $acceptedMediaTypes, array $supp * Values and splitting the options into an array list, ordered by user preference. * * @param string $rawValues The raw Accept* Header field value - * @return array The parsed list of field values, ordered by user preference + * @return array The parsed list of field values, ordered by user preference */ public static function parseContentNegotiationQualityValues(string $rawValues): array { $acceptedTypes = array_map( function ($acceptType) { $typeAndQuality = preg_split('/;\s*q=/', $acceptType); + if ($typeAndQuality === false) { + throw new \InvalidArgumentException('Invalid accept type ' . $acceptType, 1744449962); + } return [$typeAndQuality[0], (isset($typeAndQuality[1]) ? (float)$typeAndQuality[1] : '')]; }, - preg_split('/,\s*/', $rawValues) + preg_split('/,\s*/', $rawValues) ?: [] ); $flattenedAcceptedTypes = []; diff --git a/Neos.Flow/Classes/Http/Helper/RequestInformationHelper.php b/Neos.Flow/Classes/Http/Helper/RequestInformationHelper.php index db7733b833..59f13bd083 100644 --- a/Neos.Flow/Classes/Http/Helper/RequestInformationHelper.php +++ b/Neos.Flow/Classes/Http/Helper/RequestInformationHelper.php @@ -159,8 +159,8 @@ public static function getContentCharset(RequestInterface $request): string /** * Extract header key/value pairs from a $_SERVER array. * - * @param array $server - * @return array + * @param array $server + * @return array */ public static function extractHeadersFromServerVariables(array $server): array { diff --git a/Neos.Flow/Classes/Http/Helper/ResponseInformationHelper.php b/Neos.Flow/Classes/Http/Helper/ResponseInformationHelper.php index f120eba029..3b89f09a56 100644 --- a/Neos.Flow/Classes/Http/Helper/ResponseInformationHelper.php +++ b/Neos.Flow/Classes/Http/Helper/ResponseInformationHelper.php @@ -141,7 +141,7 @@ public static function generateStatusLine(ResponseInterface $response): string * Prepare array of header lines for this response * * @param ResponseInterface $response - * @return array + * @return array */ public static function prepareHeaders(ResponseInterface $response): array { @@ -230,7 +230,7 @@ public static function makeStandardsCompliant(ResponseInterface $response, Reque } if (!$response->hasHeader('Content-Length') && $response->getBody()->getSize() !== null) { - $response = $response->withHeader('Content-Length', $response->getBody()->getSize()); + $response = $response->withHeader('Content-Length', (string)$response->getBody()->getSize()); } if ($request->getMethod() === 'HEAD') { diff --git a/Neos.Flow/Classes/Http/Helper/UploadedFilesHelper.php b/Neos.Flow/Classes/Http/Helper/UploadedFilesHelper.php index 7473fbe76b..f1693789eb 100644 --- a/Neos.Flow/Classes/Http/Helper/UploadedFilesHelper.php +++ b/Neos.Flow/Classes/Http/Helper/UploadedFilesHelper.php @@ -24,9 +24,9 @@ abstract class UploadedFilesHelper { /** * @param UploadedFileInterface[]|mixed[][] $uploadedFiles A (deep) array of UploadedFile or an untangled $_FILES array - * @param array $arguments - * @param array $currentPath internal argument for recursion - * @return array The nested array of paths and uploaded files + * @param array $arguments + * @param array $currentPath internal argument for recursion + * @return array The nested array of paths and uploaded files */ public static function upcastUploadedFiles(array $uploadedFiles, array $arguments, array $currentPath = []): array { @@ -64,7 +64,7 @@ public static function upcastUploadedFiles(array $uploadedFiles, array $argument /** * @param UploadedFileInterface $uploadedFile - * @param string|array $originallySubmittedResource + * @param string|array{__identity: string} $originallySubmittedResource * @param string $collectionName * @return FlowUploadedFile */ @@ -87,8 +87,8 @@ protected static function upcastUploadedFile(UploadedFileInterface $uploadedFile /** * Transforms the convoluted _FILES superglobal into a manageable form. * - * @param array $convolutedFiles The _FILES superglobal or something with the same structure - * @return array Untangled files + * @param array $convolutedFiles The _FILES superglobal or something with the same structure + * @return array Untangled files */ public static function untangleFilesArray(array $convolutedFiles): array { @@ -124,9 +124,9 @@ public static function untangleFilesArray(array $convolutedFiles): array /** * Returns an array of all possible "field paths" for the given array. * - * @param array $structure The array to walk through + * @param array $structure The array to walk through * @param string $firstLevelFieldName - * @return array An array of paths (as arrays) in the format ["key1", "key2", "key3"] ... + * @return array An array of paths (as arrays) in the format ["key1", "key2", "key3"] ... */ protected static function calculateFieldPathsAsArray(array $structure, ?string $firstLevelFieldName = null): array { diff --git a/Neos.Flow/Classes/Http/Middleware/MethodOverrideMiddleware.php b/Neos.Flow/Classes/Http/Middleware/MethodOverrideMiddleware.php index 2aa13861b8..27cb74444a 100644 --- a/Neos.Flow/Classes/Http/Middleware/MethodOverrideMiddleware.php +++ b/Neos.Flow/Classes/Http/Middleware/MethodOverrideMiddleware.php @@ -25,7 +25,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface { if ($request->getMethod() === 'POST') { $parsedBody = $request->getParsedBody(); - if (isset($parsedBody['__method'])) { + if (is_array($parsedBody) && isset($parsedBody['__method'])) { $request = $request->withMethod($parsedBody['__method']); } elseif ($request->hasHeader('X-Http-Method-Override')) { $request = $request->withMethod($request->getHeaderLine('X-Http-Method-Override')); diff --git a/Neos.Flow/Classes/Http/Middleware/MiddlewaresChain.php b/Neos.Flow/Classes/Http/Middleware/MiddlewaresChain.php index 97cc908f09..cf7e7dbdc4 100644 --- a/Neos.Flow/Classes/Http/Middleware/MiddlewaresChain.php +++ b/Neos.Flow/Classes/Http/Middleware/MiddlewaresChain.php @@ -31,13 +31,8 @@ final class MiddlewaresChain implements RequestHandlerInterface */ private $stepCallbacks = []; - public function __construct(array $middlewaresChain) + public function __construct(MiddlewareInterface ...$middlewaresChain) { - array_walk($middlewaresChain, static function ($middleware) { - if (!$middleware instanceof MiddlewareInterface) { - throw new Exception(sprintf('Invalid element "%s" in middleware chain. Must implement %s.', is_object($middleware) ? get_class($middleware) : gettype($middleware), MiddlewareInterface::class)); - } - }); $this->chain = $middlewaresChain; } diff --git a/Neos.Flow/Classes/Http/Middleware/MiddlewaresChainFactory.php b/Neos.Flow/Classes/Http/Middleware/MiddlewaresChainFactory.php index b85e51de3a..37a387fe59 100644 --- a/Neos.Flow/Classes/Http/Middleware/MiddlewaresChainFactory.php +++ b/Neos.Flow/Classes/Http/Middleware/MiddlewaresChainFactory.php @@ -32,7 +32,7 @@ class MiddlewaresChainFactory protected $objectManager; /** - * @param array $chainConfiguration + * @param array $chainConfiguration * @return MiddlewaresChain * @throws Exception */ @@ -53,6 +53,6 @@ public function create(array $chainConfiguration): MiddlewaresChain $middlewaresChain[] = $middleware; } - return new MiddlewaresChain($middlewaresChain); + return new MiddlewaresChain(...$middlewaresChain); } } diff --git a/Neos.Flow/Classes/Http/Middleware/RequestBodyParsingMiddleware.php b/Neos.Flow/Classes/Http/Middleware/RequestBodyParsingMiddleware.php index 748372fdc2..86e3c28f6b 100644 --- a/Neos.Flow/Classes/Http/Middleware/RequestBodyParsingMiddleware.php +++ b/Neos.Flow/Classes/Http/Middleware/RequestBodyParsingMiddleware.php @@ -33,26 +33,26 @@ class RequestBodyParsingMiddleware implements MiddlewareInterface /** * @inheritDoc */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if (!empty($request->getParsedBody())) { - return $next->handle($request); + return $handler->handle($request); } $parsedBody = $this->parseRequestBody($request); - return $next->handle($request->withParsedBody($parsedBody)); + return $handler->handle($request->withParsedBody($parsedBody)); } /** * Parses the request body according to the media type. * * @param ServerRequestInterface $httpRequest - * @return null|array|string|integer + * @return null|array */ protected function parseRequestBody(ServerRequestInterface $httpRequest) { $requestBody = $httpRequest->getBody()->getContents(); - if ($requestBody === null || $requestBody === '') { - return $requestBody; + if ($requestBody === '') { + return null; } /** @var MediaTypeConverterInterface $mediaTypeConverter */ @@ -62,6 +62,9 @@ protected function parseRequestBody(ServerRequestInterface $httpRequest) $requestedContentType = $httpRequest->getHeaderLine('Content-Type'); $propertyMappingConfiguration->setTypeConverterOption(MediaTypeConverterInterface::class, MediaTypeConverterInterface::CONFIGURATION_MEDIA_TYPE, $requestedContentType); // FIXME: The MediaTypeConverter returns an empty array for "error cases", which might be unintended - return $this->propertyMapper->convert($requestBody, 'array', $propertyMappingConfiguration); + /** @var array $array */ + $array = $this->propertyMapper->convert($requestBody, 'array', $propertyMappingConfiguration); + + return $array; } } diff --git a/Neos.Flow/Classes/Http/Middleware/SecurityEntryPointMiddleware.php b/Neos.Flow/Classes/Http/Middleware/SecurityEntryPointMiddleware.php index 0f82567b99..8bfcb76bd5 100644 --- a/Neos.Flow/Classes/Http/Middleware/SecurityEntryPointMiddleware.php +++ b/Neos.Flow/Classes/Http/Middleware/SecurityEntryPointMiddleware.php @@ -8,6 +8,7 @@ use Neos\Flow\Http\ServerRequestAttributes; use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Flow\Mvc\ActionRequestFactory; +use Neos\Flow\Security\Authentication\EntryPointInterface; use Neos\Flow\Security\Authentication\Token\SessionlessTokenInterface; use Neos\Flow\Security\Authentication\TokenInterface; use Neos\Flow\Security\Context; @@ -55,7 +56,6 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface try { return $next->handle($request->withAttribute(ServerRequestAttributes::ACTION_REQUEST, $actionRequest)); } catch (AuthenticationRequiredException $authenticationException) { - /** @var TokenInterface[] $tokensWithEntryPoint */ $tokensWithEntryPoint = array_filter($this->securityContext->getAuthenticationTokens(), static function (TokenInterface $token) { return $token->getAuthenticationEntryPoint() !== null; }); @@ -72,6 +72,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } $entryPoint = $token->getAuthenticationEntryPoint(); + assert($entryPoint instanceof EntryPointInterface); $this->securityLogger->debug(sprintf('Starting authentication with entry point of type "%s"', \get_class($entryPoint)), LogEnvironment::fromMethodName(__METHOD__)); // Only store the intercepted request if it is a GET request (otherwise it can't be resumed properly) diff --git a/Neos.Flow/Classes/Http/Middleware/SessionMiddleware.php b/Neos.Flow/Classes/Http/Middleware/SessionMiddleware.php index d143b52c08..824188e29b 100644 --- a/Neos.Flow/Classes/Http/Middleware/SessionMiddleware.php +++ b/Neos.Flow/Classes/Http/Middleware/SessionMiddleware.php @@ -31,7 +31,8 @@ class SessionMiddleware implements MiddlewareInterface { /** * @Flow\InjectConfiguration(package="Neos.Flow", path="session") - * @var array + * @todo enforce with extractor + * @var array{name: string, cookie: array{lifetime: int, domain: string, path: string, secure: bool, httponly: true, samesite: string}} */ protected $sessionSettings; @@ -41,20 +42,19 @@ class SessionMiddleware implements MiddlewareInterface */ protected $sessionManager; - public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if (!$this->sessionManager instanceof SessionManager) { - return $next->handle($request); + return $handler->handle($request); } $sessionCookieName = $this->sessionSettings['name']; - /** @var ServerRequestInterface $request */ $cookies = $request->getCookieParams(); if (!isset($cookies[$sessionCookieName])) { $sessionCookie = $this->prepareCookie($sessionCookieName, Algorithms::generateRandomString(32)); $this->sessionManager->createCurrentSessionFromCookie($sessionCookie); - return $this->handleSetCookie($next->handle($request)); + return $this->handleSetCookie($handler->handle($request)); } $sessionIdentifier = $cookies[$sessionCookieName]; @@ -62,7 +62,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $this->sessionManager->initializeCurrentSessionFromCookie($sessionCookie); $this->sessionManager->getCurrentSession()->resume(); - return $this->handleSetCookie($next->handle($request)); + return $this->handleSetCookie($handler->handle($request)); } /** diff --git a/Neos.Flow/Classes/Http/Middleware/StandardsComplianceMiddleware.php b/Neos.Flow/Classes/Http/Middleware/StandardsComplianceMiddleware.php index 75e445663e..f0199ec064 100644 --- a/Neos.Flow/Classes/Http/Middleware/StandardsComplianceMiddleware.php +++ b/Neos.Flow/Classes/Http/Middleware/StandardsComplianceMiddleware.php @@ -26,12 +26,12 @@ class StandardsComplianceMiddleware implements MiddlewareInterface { /** - * @var array + * @var array */ protected $options; /** - * @param array $options + * @param array $options */ public function __construct(array $options = []) { diff --git a/Neos.Flow/Classes/Http/Middleware/TrustedProxiesMiddleware.php b/Neos.Flow/Classes/Http/Middleware/TrustedProxiesMiddleware.php index b70ab143a1..7a8f26603e 100644 --- a/Neos.Flow/Classes/Http/Middleware/TrustedProxiesMiddleware.php +++ b/Neos.Flow/Classes/Http/Middleware/TrustedProxiesMiddleware.php @@ -38,14 +38,15 @@ class TrustedProxiesMiddleware implements MiddlewareInterface const HOST_PATTERN = '(?:host=(?"[^"]+"|[0-9a-z_\.:\-]+))'; /** - * @var array + * @var array{proxies: "*"|array, headers?: string|array} */ protected $settings; /** * Injects the configuration settings * - * @param array $settings + * @todo enforce with extractor + * @param array $settings * @return void * @throws InvalidConfigurationException */ @@ -113,8 +114,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } /** - * @param array $array - * @return array + * @param array $array + * @return array */ protected function unquoteArray($array): array { @@ -125,8 +126,8 @@ protected function unquoteArray($array): array /** * @param string $type The header value type to retrieve from the Forwarded header value. One of the HEADER_* constants. - * @param array $headerValues The Forwarded header value, e.g. "for=192.168.178.5; host=www.acme.org:8080" - * @return array|null The array of values for the header type or null if the header + * @param array $headerValues The Forwarded header value, e.g. "for=192.168.178.5; host=www.acme.org:8080" + * @return array|null The array of values for the header type or null if the header */ protected function getForwardedHeader($type, array $headerValues) { @@ -139,6 +140,9 @@ protected function getForwardedHeader($type, array $headerValues) return null; } $headerValue = reset($headerValues); + if (!$headerValue) { + return null; + } preg_match_all('/' . $patterns[$type] . '/i', $headerValue, $matches); $matchedHeader = $this->unquoteArray($matches[1]); if ($matchedHeader === []) { @@ -152,7 +156,7 @@ protected function getForwardedHeader($type, array $headerValues) * * @param string $type One of the HEADER_* constants * @param ServerRequestInterface $request The request to get the trusted proxy header from - * @return \Generator Yields the array of the values for this header type or yields NULL if this header type should not be trusted + * @return \Generator|null> Yields the array of the values for this header type or yields NULL if this header type should not be trusted */ protected function getTrustedProxyHeaderValues($type, ServerRequestInterface $request) { diff --git a/Neos.Flow/Classes/Http/RequestHandler.php b/Neos.Flow/Classes/Http/RequestHandler.php index 58255533ab..eb8561ad5b 100644 --- a/Neos.Flow/Classes/Http/RequestHandler.php +++ b/Neos.Flow/Classes/Http/RequestHandler.php @@ -36,7 +36,7 @@ class RequestHandler implements HttpRequestHandlerInterface protected $middlewaresChain; /** - * @var ServerRequestInterface + * @var ?ServerRequestInterface */ protected $httpRequest; @@ -89,6 +89,7 @@ public function getPriority() /** * Handles a HTTP request * + * @phpstan-assert ServerRequest $this->httpRequest * @return void */ public function handleRequest() @@ -102,6 +103,7 @@ public function handleRequest() $this->middlewaresChain->onStep(function (ServerRequestInterface $request) { $this->httpRequest = $request; }); + assert($this->httpRequest instanceof ServerRequestInterface); $this->httpResponse = $this->middlewaresChain->handle($this->httpRequest); $this->sendResponse($this->httpResponse); @@ -112,7 +114,7 @@ public function handleRequest() /** * Returns the currently handled HTTP request * - * @return ServerRequestInterface + * @return ?ServerRequestInterface * @api */ public function getHttpRequest() @@ -148,7 +150,7 @@ protected function resolveDependencies() * Send the HttpResponse of the component context to the browser and flush all output buffers. * @param ResponseInterface $response */ - protected function sendResponse(ResponseInterface $response) + protected function sendResponse(ResponseInterface $response): void { ob_implicit_flush(); foreach (ResponseInformationHelper::prepareHeaders($response) as $prepareHeader) { diff --git a/Neos.Flow/Classes/Http/UploadedFile.php b/Neos.Flow/Classes/Http/UploadedFile.php index 90d9139dff..399116c800 100644 --- a/Neos.Flow/Classes/Http/UploadedFile.php +++ b/Neos.Flow/Classes/Http/UploadedFile.php @@ -16,12 +16,12 @@ class UploadedFile implements UploadedFileInterface { /** - * @var string + * @var ?string */ protected $clientFilename; /** - * @var string + * @var ?string */ protected $clientMediaType; @@ -75,7 +75,7 @@ public function __construct($streamOrFile, $size, $errorStatus, $clientFilename * @param string|StreamInterface|resource $streamOrFile * @throws InvalidArgumentException */ - protected function setStreamOrFile($streamOrFile) + protected function setStreamOrFile($streamOrFile): void { if (is_string($streamOrFile)) { $this->file = $streamOrFile; @@ -135,7 +135,14 @@ public function getStream() return $this->stream; } - return new Stream(fopen($this->file, 'rb+')); + if (!$this->file) { + throw new \RuntimeException('Missing file ' . $this->file, 1744443740); + } + $resource = fopen($this->file, 'rb+'); + if (!$resource) { + throw new \RuntimeException('Failed to open file ' . $this->file, 1744442831); + } + return new Stream($resource); } /** @@ -172,18 +179,20 @@ public function getStream() * the second or subsequent call to the method. * @api PSR-7 */ - public function moveTo($targetPath) + public function moveTo(string $targetPath): void { $this->throwExceptionIfNotAccessible(); - if (!is_string($targetPath) || empty($targetPath)) { + if (empty($targetPath)) { throw new InvalidArgumentException('Invalid path provided to move uploaded file to. Must be a non-empty string', 1479747624); } + /** @phpstan-ignore identical.alwaysTrue (FLOW_SAPITYPE can also be "Web") */ if ($this->stream !== null || ($this->file !== null && FLOW_SAPITYPE === 'CLI')) { $this->moved = $this->writeFile($targetPath); } + /** @phpstan-ignore notIdentical.alwaysFalse, booleanAnd.alwaysFalse (FLOW_SAPITYPE can also be "Web", thus this can be true) */ if ($this->file !== null && FLOW_SAPITYPE !== 'CLI') { $this->moved = move_uploaded_file($this->file, $targetPath); } @@ -267,7 +276,7 @@ public function getClientMediaType() /** * @throws RuntimeException if is moved or not ok */ - protected function throwExceptionIfNotAccessible() + protected function throwExceptionIfNotAccessible(): void { if (!$this->isOk()) { throw new RuntimeException('UploadedFile has the following error: ' . Files::getUploadErrorMessage($this->error), 1479743608); diff --git a/Neos.Flow/Classes/Http/UriTemplate.php b/Neos.Flow/Classes/Http/UriTemplate.php index 085fe134b7..1d7598518f 100644 --- a/Neos.Flow/Classes/Http/UriTemplate.php +++ b/Neos.Flow/Classes/Http/UriTemplate.php @@ -22,24 +22,24 @@ class UriTemplate { /** - * @var array + * @var array */ protected static $variables; /** - * @var array + * @var array */ protected static $operators = [ '+' => true, '#' => true, '.' => true, '/' => true, ';' => true, '?' => true, '&' => true ]; /** - * @var array + * @var array */ protected static $delimiters = [':', '/', '?', '#', '[', ']', '@', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=']; /** - * @var array + * @var array */ protected static $encodedDelimiters = ['%3A', '%2F', '%3F', '%23', '%5B', '%5D', '%40', '%21', '%24', '%26', '%27', '%28', '%29', '%2A', '%2B', '%2C', '%3B', '%3D']; @@ -47,7 +47,7 @@ class UriTemplate * Expand the template string using the supplied variables * * @param string $template URI template to expand - * @param array $variables variables to use with the expansion + * @param array $variables variables to use with the expansion * @return string */ public static function expand($template, array $variables) @@ -58,13 +58,13 @@ public static function expand($template, array $variables) self::$variables = $variables; - return preg_replace_callback('/\{([^\}]+)\}/', [UriTemplate::class, 'expandMatch'], $template); + return preg_replace_callback('/\{([^\}]+)\}/', [UriTemplate::class, 'expandMatch'], $template) ?: ''; } /** * Process an expansion * - * @param array $matches matches found in preg_replace_callback + * @param array{1: string} $matches matches found in preg_replace_callback * @return string replacement string */ protected static function expandMatch(array $matches) @@ -98,7 +98,6 @@ protected static function expandMatch(array $matches) if (!array_key_exists($value['value'], self::$variables) || self::$variables[$value['value']] === null) { continue; } - $variable = self::$variables[$value['value']]; $useQueryString = $queryStringShouldBeUsed; @@ -106,7 +105,7 @@ protected static function expandMatch(array $matches) $expanded = self::encodeArrayVariable($variable, $value, $parsed['operator'], $separator, $useQueryString); } else { if ($value['modifier'] === ':') { - $variable = substr($variable, 0, $value['position']); + $variable = substr($variable, 0, $value['position'] ?? 0); } $expanded = rawurlencode($variable); if ($parsed['operator'] === '+' || $parsed['operator'] === '#') { @@ -137,7 +136,7 @@ protected static function expandMatch(array $matches) * Parse an expression into parts * * @param string $expression Expression to parse - * @return array associative array of parts + * @return array{operator: string, values: array} associative array of parts */ protected static function parseExpression($expression) { @@ -169,6 +168,7 @@ protected static function parseExpression($expression) $expressionPart = $configuration; } + /** @phpstan-ignore return.type (values resolved by reference) */ return [ 'operator' => $operator, 'values' => $explodedExpression @@ -178,8 +178,8 @@ protected static function parseExpression($expression) /** * Encode arrays for use in the expanded URI string * - * @param array $variable - * @param array $value + * @param array $variable + * @param array $value * @param string $operator * @param string $separator * @param bool $useQueryString @@ -247,7 +247,7 @@ protected static function encodeArrayVariable(array $variable, array $value, $op /** * Determines if an array is associative, i.e. contains at least one key that is a string. * - * @param array $array + * @param array $array * @return boolean */ protected static function isAssociative(array $array) diff --git a/Neos.Flow/Classes/I18n/AbstractXmlParser.php b/Neos.Flow/Classes/I18n/AbstractXmlParser.php index 597f531284..8e64778f2d 100644 --- a/Neos.Flow/Classes/I18n/AbstractXmlParser.php +++ b/Neos.Flow/Classes/I18n/AbstractXmlParser.php @@ -23,7 +23,7 @@ abstract class AbstractXmlParser /** * Associative array of "filename => parsed data" pairs. * - * @var array + * @var array> */ protected $parsedFiles; @@ -33,7 +33,7 @@ abstract class AbstractXmlParser * Parses XML if it wasn't done before. Caches parsed data. * * @param string $sourcePath An absolute path to XML file - * @return array Parsed XML file + * @return array Parsed XML file */ public function getParsedData(string $sourcePath) { @@ -56,7 +56,7 @@ protected function getRootNode(string $sourcePath): \SimpleXMLElement } libxml_use_internal_errors(true); // Use of simplexml_load_string/file_get_contents ia a workaround for https://bugs.php.net/bug.php?id=62577 - $rootXmlNode = simplexml_load_string(file_get_contents($sourcePath), 'SimpleXmlElement', \LIBXML_NOWARNING); + $rootXmlNode = simplexml_load_string(file_get_contents($sourcePath) ?: '', 'SimpleXmlElement', \LIBXML_NOWARNING); if ($rootXmlNode === false) { $errors = []; foreach (libxml_get_errors() as $error) { @@ -76,7 +76,7 @@ protected function getRootNode(string $sourcePath): \SimpleXMLElement * Reads and parses XML file and returns internal representation of data. * * @param string $sourcePath An absolute path to XML file - * @return array Parsed XML file + * @return array Parsed XML file */ protected function parseXmlFile(string $sourcePath) { @@ -89,7 +89,7 @@ protected function parseXmlFile(string $sourcePath) * Returns array representation of XML data, starting from a root node. * * @param \SimpleXMLElement $root A root node - * @return array An array representing parsed XML file (structure depends on concrete parser) + * @return array An array representing parsed XML file (structure depends on concrete parser) */ abstract protected function doParsingFromRoot(\SimpleXMLElement $root); } diff --git a/Neos.Flow/Classes/I18n/Cldr/CldrModel.php b/Neos.Flow/Classes/I18n/Cldr/CldrModel.php index 5e4a6ab33b..ecd32cdc73 100644 --- a/Neos.Flow/Classes/I18n/Cldr/CldrModel.php +++ b/Neos.Flow/Classes/I18n/Cldr/CldrModel.php @@ -56,7 +56,7 @@ class CldrModel protected $sourcePaths; /** - * @var array + * @var array */ protected $parsedData; @@ -276,7 +276,7 @@ public static function getAttributeValue(string $nodeString, string $attributeNa * Merging is done with inheritance in mind, as defined in CLDR specification. * * @param array $sourcePaths Absolute paths to CLDR files (can be one file) - * @return array Parsed and merged data + * @return array Parsed and merged data */ protected function parseFiles(array $sourcePaths): array { @@ -304,7 +304,7 @@ protected function parseFiles(array $sourcePaths): array * * @param mixed $firstParsedData Part of data from first file (either array or string) * @param mixed $secondParsedData Part of data from second file (either array or string) - * @return array Data merged from two files + * @return array Data merged from two files */ protected function mergeTwoParsedFiles($firstParsedData, $secondParsedData) { @@ -351,6 +351,9 @@ protected function resolveAliases($data, string $currentPath) } $sourcePath = self::getAttributeValue($nodeString, 'path'); + if (!is_string($sourcePath)) { + throw new \Exception('source path must be string', 1744412220); + } // Change relative path to absolute one $sourcePath = str_replace('../', '', $sourcePath, $countOfJumpsToParentNode); diff --git a/Neos.Flow/Classes/I18n/Cldr/CldrParser.php b/Neos.Flow/Classes/I18n/Cldr/CldrParser.php index 5a7ec6bb2f..bf2fe58844 100644 --- a/Neos.Flow/Classes/I18n/Cldr/CldrParser.php +++ b/Neos.Flow/Classes/I18n/Cldr/CldrParser.php @@ -56,7 +56,7 @@ class CldrParser extends AbstractXmlParser * Returns array representation of XML data, starting from a root node. * * @param \SimpleXMLElement $root A root node - * @return array An array representing parsed CLDR File + * @return array An array representing parsed CLDR File * @see AbstractXmlParser::doParsingFromRoot() */ protected function doParsingFromRoot(\SimpleXMLElement $root) diff --git a/Neos.Flow/Classes/I18n/Cldr/CldrRepository.php b/Neos.Flow/Classes/I18n/Cldr/CldrRepository.php index 54403980c0..b14dc6d8c8 100644 --- a/Neos.Flow/Classes/I18n/Cldr/CldrRepository.php +++ b/Neos.Flow/Classes/I18n/Cldr/CldrRepository.php @@ -51,7 +51,7 @@ class CldrRepository * reside and 'locale' is used to define which files are included in the * relation (e.g. for locale 'en_GB' files would be: root + en + en_GB). * - * @var array + * @var array> */ protected $models; @@ -73,13 +73,13 @@ public function injectLocalizationService(I18n\Service $localizationService) * file. * * @param string $filename Relative (from CLDR root) path to existing CLDR file - * @return CldrModel|false A CldrModel instance or false on failure + * @return CldrModel|false An array of CldrModel instances indexed by Locale or false on failure */ public function getModel($filename) { $filename = Files::concatenatePaths([$this->cldrBasePath, $filename . '.xml']); - if (isset($this->models[$filename])) { + if (isset($this->models[$filename]) && $this->models[$filename] instanceof CldrModel) { return $this->models[$filename]; } @@ -110,7 +110,7 @@ public function getModelForLocale(I18n\Locale $locale, $directoryPath = 'main') { $directoryPath = Files::concatenatePaths([$this->cldrBasePath, $directoryPath]); - if (isset($this->models[$directoryPath][(string)$locale])) { + if (is_array($this->models[$directoryPath]) && isset($this->models[$directoryPath][(string)$locale])) { return $this->models[$directoryPath][(string)$locale]; } @@ -120,6 +120,11 @@ public function getModelForLocale(I18n\Locale $locale, $directoryPath = 'main') $filesInHierarchy = $this->findLocaleChain($locale, $directoryPath); + if (($this->models[$directoryPath] ?? null) instanceof CldrModel) { + throw new \Exception('Directory path ' . $directoryPath . ' is already occupied', 1744414384); + } + + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (we already checked that above. also @todo separate the arrays) */ return $this->models[$directoryPath][(string)$locale] = new CldrModel($filesInHierarchy); } diff --git a/Neos.Flow/Classes/I18n/Cldr/Reader/CurrencyReader.php b/Neos.Flow/Classes/I18n/Cldr/Reader/CurrencyReader.php index 37d7351262..5f761fad05 100644 --- a/Neos.Flow/Classes/I18n/Cldr/Reader/CurrencyReader.php +++ b/Neos.Flow/Classes/I18n/Cldr/Reader/CurrencyReader.php @@ -37,7 +37,7 @@ class CurrencyReader /** * An array of fractions data, indexed by currency code. * - * @var array + * @var array */ protected $fractions; @@ -81,7 +81,7 @@ public function initializeObject() * Returns an array with keys "digits" and "rounding", each an integer. * * @param string $currencyCode - * @return array ['digits' => int, 'rounding => int] + * @return array{digits: int, rounding: int} */ public function getFraction(string $currencyCode): array { @@ -102,6 +102,9 @@ public function getFraction(string $currencyCode): array protected function generateFractions(): void { $model = $this->cldrRepository->getModel('supplemental/supplementalData'); + if (!$model) { + throw new \Exception('Missing model for supplemental/supplementalData', 1744411964); + } $currencyData = $model->getRawArray('currencyData'); foreach ($currencyData['fractions'] as $fractionString) { diff --git a/Neos.Flow/Classes/I18n/Cldr/Reader/DatesReader.php b/Neos.Flow/Classes/I18n/Cldr/Reader/DatesReader.php index cc62041fb5..379ee695a0 100644 --- a/Neos.Flow/Classes/I18n/Cldr/Reader/DatesReader.php +++ b/Neos.Flow/Classes/I18n/Cldr/Reader/DatesReader.php @@ -108,7 +108,7 @@ class DatesReader * When length is set to zero, it means that corresponding format has no * maximal length. * - * @var array + * @var array */ protected static $maxLengthOfSubformats = [ 'G' => 5, @@ -163,7 +163,7 @@ class DatesReader * are stored as one-element arrays in the same array. Order of elements * in array is important. * - * @var array + * @var array */ protected $parsedFormats; @@ -184,7 +184,7 @@ class DatesReader * ... * ); * - * @var array + * @var array */ protected $parsedFormatsIndices; @@ -194,7 +194,7 @@ class DatesReader * Locale tags are keys for this array. Values are arrays of literals, i.e. * names defined in months, days, quarters etc tags. * - * @var array + * @var array */ protected $localizedLiterals; @@ -255,7 +255,7 @@ public function shutdownObject() * @param Locale $locale * @param string $formatType A type of format (one of constant values) * @param string $formatLength A length of format (one of constant values) - * @return array An array representing parsed format + * @return array An array representing parsed format * @throws Exception\InvalidDateTimeFormatException * @throws Exception\InvalidFormatLengthException * @throws Exception\InvalidFormatTypeException @@ -273,6 +273,9 @@ public function parseFormatFromCldr(Locale $locale, string $formatType, string $ } $model = $this->cldrRepository->getModelForLocale($locale); + if (!$model) { + throw new \Exception('Missing model for locale ' . $locale, 1744411881); + } $realFormatLength = $formatLength === '' || $formatLength === 'default' ? 'medium' : $formatLength; $format = $model->getElement('dates/calendars/calendar[@type="gregorian"]/' . $formatType . 'Formats/' . $formatType . 'FormatLength[@type="' . $realFormatLength . '"]/' . $formatType . 'Format/pattern'); @@ -301,7 +304,7 @@ public function parseFormatFromCldr(Locale $locale, string $formatType, string $ * Returns parsed date or time format string provided as parameter. * * @param string $format Format string to parse - * @return array An array representing parsed format + * @return array An array representing parsed format * @throws Exception\InvalidDateTimeFormatException */ public function parseCustomFormat(string $format): array @@ -319,7 +322,7 @@ public function parseCustomFormat(string $format): array * If array was not generated earlier, it will be generated and cached. * * @param Locale $locale - * @return array An array with localized literals + * @return array An array with localized literals */ public function getLocalizedLiteralsForLocale(Locale $locale): array { @@ -328,6 +331,9 @@ public function getLocalizedLiteralsForLocale(Locale $locale): array } $model = $this->cldrRepository->getModelForLocale($locale); + if (!$model) { + throw new \Exception('Missing model for locale ' . $locale, 1744411830); + } $localizedLiterals['months'] = $this->parseLocalizedLiterals($model, 'month'); $localizedLiterals['days'] = $this->parseLocalizedLiterals($model, 'day'); @@ -375,7 +381,7 @@ public static function validateFormatLength(string $formatLength): void * documentation for this class for details what is missing. * * @param string $format - * @return array Parsed format + * @return array Parsed format * @throws Exception\InvalidDateTimeFormatException When subformat is longer than maximal value defined in $maxLengthOfSubformats property * @see DatesReader::$parsedFormats */ @@ -448,7 +454,7 @@ protected function parseFormat(string $format): array * * @param CldrModel $model CldrModel to read data from * @param string $literalType One of: month, day, quarter, dayPeriod - * @return array An array with localized literals for given type + * @return array An array with localized literals for given type * @todo the two array checks should go away - but that needs clean input data */ protected function parseLocalizedLiterals(CldrModel $model, string $literalType): array @@ -481,7 +487,7 @@ protected function parseLocalizedLiterals(CldrModel $model, string $literalType) * Parses "eras" child of "dates" node and returns it's array representation. * * @param CldrModel $model CldrModel to read data from - * @return array An array with localized literals for "eras" node + * @return array An array with localized literals for "eras" node */ protected function parseLocalizedEras(CldrModel $model): array { @@ -508,7 +514,7 @@ protected function parseLocalizedEras(CldrModel $model): array * @param string $format DateTime format * @param Locale $locale Locale to use * @param string $formatLength A length of format (full, long, medium, short) or 'default' to use default one from CLDR - * @return array Merged formats of date and time + * @return array Merged formats of date and time * @throws Exception\InvalidDateTimeFormatException * @throws Exception\InvalidFormatLengthException * @throws Exception\InvalidFormatTypeException @@ -538,7 +544,7 @@ protected function prepareDateAndTimeFormat(string $format, Locale $locale, stri if ($positionOfFirstPlaceholder !== 0) { // Add everything before placeholder as literal - $parsedFormat[] = [substr($format, 0, $positionOfFirstPlaceholder)]; + $parsedFormat[] = [substr($format, 0, $positionOfFirstPlaceholder ?: 0)]; } $parsedFormat = array_merge($parsedFormat, $firstParsedFormat); diff --git a/Neos.Flow/Classes/I18n/Cldr/Reader/NumbersReader.php b/Neos.Flow/Classes/I18n/Cldr/Reader/NumbersReader.php index 86bd26f4da..e38fe70cbb 100644 --- a/Neos.Flow/Classes/I18n/Cldr/Reader/NumbersReader.php +++ b/Neos.Flow/Classes/I18n/Cldr/Reader/NumbersReader.php @@ -33,6 +33,19 @@ * - formatting numbers in other number systems than "latn" * - currency symbol substitution is simplified * + * @phpstan-type ParsedFormat array{ + * positivePrefix: string, + * positiveSuffix: string, + * negativePrefix: string, + * negativeSuffix: string, + * multiplier: int, + * minDecimalDigits: int, + * maxDecimalDigits: int, + * minIntegerDigits: int, + * primaryGroupingSize: int, + * secondaryGroupingSize: int, + * rounding: float + * } * @Flow\Scope("singleton") * @see http://www.unicode.org/reports/tr35/#Number_Elements * @see http://www.unicode.org/reports/tr35/#Number_Format_Patterns @@ -126,7 +139,7 @@ class NumbersReader * localized during formatting (eg minus sign, percent etc), or other chars * which will be used as-is. * - * @var array + * @var array */ protected $parsedFormats; @@ -147,7 +160,7 @@ class NumbersReader * ... * ); * - * @var array + * @var array>> */ protected $parsedFormatsIndices; @@ -157,7 +170,7 @@ class NumbersReader * Locale identifiers are keys for this array. Values are arrays of symbols, * as defined in /ldml/numbers/symbols path in CLDR files. * - * @var array + * @var array> */ protected $localizedSymbols; @@ -218,7 +231,7 @@ public function shutdownObject(): void * @param Locale $locale * @param string $formatType A type of format (one of constant values) * @param string $formatLength A length of format (one of constant values) - * @return array An array representing parsed format + * @return ParsedFormat An array representing parsed format * @throws Exception\InvalidFormatLengthException * @throws Exception\InvalidFormatTypeException * @throws Exception\UnableToFindFormatException When there is no proper format string in CLDR @@ -245,6 +258,9 @@ public function parseFormatFromCldr(Locale $locale, string $formatType, string $ } $model = $this->cldrRepository->getModelForLocale($locale); + if (!$model) { + throw new \Exception('Missing model for locale ' . $locale, 1744411694); + } $format = $model->getElement($formatPath); if (empty($format)) { @@ -264,6 +280,9 @@ public function parseFormatFromCldr(Locale $locale, string $formatType, string $ public function getDefaultNumberingSystem(Locale $locale): string { $model = $this->cldrRepository->getModelForLocale($locale); + if (!$model) { + throw new \Exception('Missing model for locale '. $locale, 1744411670); + } $result = $model->findNodesWithinPath('numbers', 'defaultNumberingSystem'); return $result['defaultNumberingSystem'] ?? 'latn'; } @@ -272,7 +291,7 @@ public function getDefaultNumberingSystem(Locale $locale): string * Returns parsed date or time format string provided as parameter. * * @param string $format Format string to parse - * @return array An array representing parsed format + * @return ParsedFormat An array representing parsed format * @throws Exception\UnsupportedNumberFormatException */ public function parseCustomFormat(string $format): array @@ -294,15 +313,19 @@ public function parseCustomFormat(string $format): array * Symbols arrays for every requested locale are cached. * * @param Locale $locale - * @return array Symbols array + * @return array{decimal: string} Symbols array */ public function getLocalizedSymbolsForLocale(Locale $locale): array { if (isset($this->localizedSymbols[(string)$locale])) { + /** @phpstan-ignore return.type (@todo enforce with extractor) */ return $this->localizedSymbols[(string)$locale]; } $model = $this->cldrRepository->getModelForLocale($locale); + if (!$model) { + throw new \Exception('missing model for locale ' . $locale, 1744411617); + } return $this->localizedSymbols[(string)$locale] = $model->getRawArray('numbers/symbols'); } @@ -343,7 +366,7 @@ public static function validateFormatLength(string $formatLength): void * documentation for this class for details what is missing. * * @param string $format - * @return array Parsed format + * @return ParsedFormat Parsed format * @throws Exception\UnsupportedNumberFormatException When unsupported format characters encountered * @see NumbersReader::$parsedFormats */ @@ -403,7 +426,7 @@ protected function parseFormat(string $format): array if (preg_match(self::PATTERN_MATCH_ROUNDING, $format, $matches) === 1) { $parsedFormat['rounding'] = (float)$matches[1]; - $format = preg_replace('/[1-9]/', '0', $format); + $format = preg_replace('/[1-9]/', '0', $format) ?: ''; } if (($positionOfDecimalSeparator = strpos($format, '.')) !== false) { diff --git a/Neos.Flow/Classes/I18n/Cldr/Reader/PluralsReader.php b/Neos.Flow/Classes/I18n/Cldr/Reader/PluralsReader.php index f33289ff5b..41feb1adb1 100644 --- a/Neos.Flow/Classes/I18n/Cldr/Reader/PluralsReader.php +++ b/Neos.Flow/Classes/I18n/Cldr/Reader/PluralsReader.php @@ -16,6 +16,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Cache\Frontend\VariableFrontend; use Neos\Flow\I18n\Cldr\CldrRepository; +use Neos\Flow\I18n\Cldr\Reader\Exception\InvalidPluralRuleException; use Neos\Flow\I18n\Locale; /** @@ -86,7 +87,7 @@ class PluralsReader * subrule in chain. If current subrule is a last one (or only one), this * element is set to false. * - * @var array + * @var array */ protected $rulesets; @@ -94,7 +95,7 @@ class PluralsReader * An associative array holding information which ruleset is used by given * locale. One or more locales can use the same ruleset. * - * @var array + * @var array */ protected $rulesetsIndices; @@ -223,7 +224,7 @@ public function getPluralForm($quantity, Locale $locale): string * Returns array of plural forms available for particular locale. * * @param Locale $locale Locale to return plural forms for - * @return array Plural forms' names (one, zero, two, few, many, other) available for language set in this model + * @return array Plural forms' names (one, zero, two, few, many, other) available for language set in this model */ public function getPluralForms(Locale $locale): array { @@ -254,6 +255,9 @@ public function getPluralForms(Locale $locale): array protected function generateRulesets(): void { $model = $this->cldrRepository->getModel('supplemental/plurals'); + if (!$model) { + throw new InvalidPluralRuleException('Missing plural rule model for supplemental/plurals', 1744411434); + } $pluralRulesSet = $model->getRawArray('plurals'); $index = 0; @@ -300,7 +304,7 @@ protected function generateRulesets(): void * $rulesets property for details). * * @param string $rule - * @return array Parsed rule + * @return array Parsed rule * @throws Exception\InvalidPluralRuleException When plural rule does not match regexp pattern */ protected function parseRule(string $rule): array @@ -319,12 +323,12 @@ protected function parseRule(string $rule): array $condition = [$matchedSubrule[3], (int)$matchedSubrule[4]]; if (!in_array($matchedSubrule[3], ['is', 'isnot'], true)) { - $condition[2] = (int)$matchedSubrule[5]; + $condition[2] = (int)($matchedSubrule[5] ?? 0); } $subrule['condition'] = $condition; - if (isset($matchedSubrule[6]) && ($matchedSubrule[6] === 'and' || $matchedSubrule[6] === 'or')) { + if (isset($matchedSubrule[6])) { $subrule['logicalOperator'] = $matchedSubrule[6]; } else { $subrule['logicalOperator'] = false; diff --git a/Neos.Flow/Classes/I18n/Configuration.php b/Neos.Flow/Classes/I18n/Configuration.php index 5a8352c26e..d30ffc5e6a 100644 --- a/Neos.Flow/Classes/I18n/Configuration.php +++ b/Neos.Flow/Classes/I18n/Configuration.php @@ -24,12 +24,12 @@ class Configuration protected $defaultLocale; /** - * @var Locale + * @var ?Locale */ protected $currentLocale; /** - * @var array + * @var array{strict: bool, order: array} */ protected $fallbackRule = ['strict' => false, 'order' => []]; @@ -103,9 +103,9 @@ public function getCurrentLocale() * Here is an example: * array('strict' => false, 'order' => array('dk', 'za')) * - * @param array $fallbackRule + * @param array{strict?: bool, order?: array} $fallbackRule */ - public function setFallbackRule(array $fallbackRule) + public function setFallbackRule(array $fallbackRule): void { if (!array_key_exists('order', $fallbackRule)) { throw new \InvalidArgumentException('The given fallback rule did not contain an order element.', 1406710671); @@ -119,7 +119,7 @@ public function setFallbackRule(array $fallbackRule) /** * Returns the current fallback rule. * - * @return array + * @return array * @see setFallbackRule() */ public function getFallbackRule() diff --git a/Neos.Flow/Classes/I18n/EelHelper/TranslationHelper.php b/Neos.Flow/Classes/I18n/EelHelper/TranslationHelper.php index dc71fa5b4c..2d14e01764 100644 --- a/Neos.Flow/Classes/I18n/EelHelper/TranslationHelper.php +++ b/Neos.Flow/Classes/I18n/EelHelper/TranslationHelper.php @@ -34,7 +34,7 @@ class TranslationHelper implements ProtectedContextAwareInterface * * @param string $id Id to use for finding translation (trans-unit id in XLIFF) * @param string $originalLabel The original translation value (the untranslated source string). - * @param array $arguments Array of numerically indexed or named values to be inserted into placeholders. Have a look at the internationalization documentation in the definitive guide for details. + * @param array $arguments Array of numerically indexed or named values to be inserted into placeholders. Have a look at the internationalization documentation in the definitive guide for details. * @param string $source Name of file with translations * @param string $package Target package key. If not set, the current package key will be used * @param mixed $quantity A number to find plural form for (float or int), NULL to not use plural forms @@ -98,10 +98,10 @@ public function allowsCallOfMethod($methodName) * translated label. * * @param string $id Id to use for finding translation (trans-unit id in XLIFF) - * @param string $originalLabel The original translation value (the untranslated source string). - * @param array $arguments Numerically indexed array of values to be inserted into placeholders + * @param ?string $originalLabel The original translation value (the untranslated source string). + * @param array $arguments Numerically indexed array of values to be inserted into placeholders * @param string|null $source Name of file with translations, defaults to 'Main' - * @param string $package Target package key. If not set, the current package key will be used + * @param ?string $package Target package key. If not set, the current package key will be used * @param mixed $quantity A number to find plural form for (float or int), NULL to not use plural forms * @param string $locale An identifier of locale to use (NULL for use the default locale) * @return string|null Translated label or source label / ID key @@ -111,11 +111,15 @@ protected function translateByExplicitlyPassedOrderedArguments($id, $originalLab $translationParameterToken = $this->createTranslationParameterToken($id); $source = $source === null ? 'Main' : str_replace('.', '/', $source); $translationParameterToken - ->value($originalLabel) ->arguments($arguments) ->source($source) - ->package($package) ->quantity($quantity); + if ($originalLabel) { + $translationParameterToken->value($originalLabel); + } + if ($package) { + $translationParameterToken->package($package); + } if ($locale !== null) { $translationParameterToken->locale($locale); diff --git a/Neos.Flow/Classes/I18n/EelHelper/TranslationParameterToken.php b/Neos.Flow/Classes/I18n/EelHelper/TranslationParameterToken.php index 71db0f3f0a..d43dd117bb 100644 --- a/Neos.Flow/Classes/I18n/EelHelper/TranslationParameterToken.php +++ b/Neos.Flow/Classes/I18n/EelHelper/TranslationParameterToken.php @@ -36,7 +36,7 @@ class TranslationParameterToken implements ProtectedContextAwareInterface /** * Key/Value store to keep the collected parameters * - * @var array + * @var array */ protected $parameters = []; @@ -61,7 +61,7 @@ public function __construct($id = null, $value = null) * * @param Translator $translator */ - public function injectTranslator(Translator $translator) + public function injectTranslator(Translator $translator): void { $this->translator = $translator; } @@ -93,7 +93,7 @@ public function value($value) /** * Set the arguments. * - * @param array $arguments Numerically indexed array of values to be inserted into placeholders + * @param array $arguments Numerically indexed array of values to be inserted into placeholders * @return TranslationParameterToken */ public function arguments(array $arguments) @@ -160,13 +160,14 @@ public function locale($locale) /** * Translate according to currently collected parameters * - * @param array $overrides An associative array to override the collected parameters + * @param array $overrides An associative array to override the collected parameters * @return string|null * @throws IndexOutOfBoundsException * @throws InvalidFormatPlaceholderException */ public function translate(array $overrides = []) { + /** @phpstan-ignore function.resultUnused (whatever, @todo fixme) */ array_replace_recursive($this->parameters, $overrides); $id = $this->parameters['id'] ?? null; @@ -182,7 +183,7 @@ public function translate(array $overrides = []) } $translation = $this->translator->translateById($id, $arguments, $quantity, $locale, $source, $package); - if ($translation === null && $value !== null) { + if ($translation === null) { return $this->translator->translateByOriginalLabel($value, $arguments, $quantity, $locale, $source, $package); } diff --git a/Neos.Flow/Classes/I18n/FormatResolver.php b/Neos.Flow/Classes/I18n/FormatResolver.php index c9ab23df2b..010f0324c1 100644 --- a/Neos.Flow/Classes/I18n/FormatResolver.php +++ b/Neos.Flow/Classes/I18n/FormatResolver.php @@ -58,7 +58,7 @@ class FormatResolver /** * Array of concrete formatters used by this class. * - * @var array + * @var array */ protected $formatters; @@ -91,7 +91,7 @@ public function injectLocalizationService(I18n\Service $localizationService) * specific and they are directly passed to the formatter class. * * @param string $textWithPlaceholders String message with placeholder(s) - * @param array $arguments An array of values to replace placeholders with + * @param array $arguments An array of values to replace placeholders with * @param Locale $locale Locale to use (NULL for default one) * @return string The $text with placeholders resolved * @throws Exception\InvalidFormatPlaceholderException When encountered incorrectly formatted placeholder @@ -169,8 +169,14 @@ protected function getFormatter($formatterType) if ($foundFormatter === false) { if ($this->objectManager->isRegistered($formatterType)) { $possibleClassName = $formatterType; + if (!class_exists($possibleClassName)) { + throw new \Exception('Invalid formatter ' . $possibleClassName, 1744413028); + } } else { $possibleClassName = sprintf('Neos\Flow\I18n\Formatter\%sFormatter', ucfirst($formatterType)); + if (!class_exists($possibleClassName)) { + throw new \Exception('Invalid formatter ' . $possibleClassName, 1744413028); + } if (!$this->objectManager->isRegistered($possibleClassName)) { throw new Exception\UnknownFormatterException('Could not find formatter for "' . $formatterType . '".', 1278057791); } @@ -180,6 +186,9 @@ protected function getFormatter($formatterType) } $foundFormatter = $this->objectManager->get($possibleClassName); } + if (!$foundFormatter instanceof FormatterInterface) { + throw new \Exception('No valid formatter found, got ' . get_class($foundFormatter), 1744412948); + } $this->formatters[$formatterType] = $foundFormatter; return $foundFormatter; diff --git a/Neos.Flow/Classes/I18n/Formatter/DatetimeFormatter.php b/Neos.Flow/Classes/I18n/Formatter/DatetimeFormatter.php index 2cb876c528..27346332f1 100644 --- a/Neos.Flow/Classes/I18n/Formatter/DatetimeFormatter.php +++ b/Neos.Flow/Classes/I18n/Formatter/DatetimeFormatter.php @@ -48,7 +48,7 @@ public function injectDatesReader(DatesReader $datesReader) * * @param mixed $value Formatter-specific variable to format (can be integer, \DateTime, etc) * @param Locale $locale Locale to use - * @param array $styleProperties Integer-indexed array of formatter-specific style properties (can be empty) + * @param array $styleProperties Integer-indexed array of formatter-specific style properties (can be empty) * @return string String representation of $value provided, or (string)$value * @api */ @@ -157,8 +157,8 @@ public function formatDateTime(\DateTimeInterface $dateTime, Locale $locale, $fo * are replaced with elements from $localizedLiterals array. * * @param \DateTimeInterface $dateTime PHP object representing particular point in time - * @param array $parsedFormat An array describing format (as in $parsedFormats property) - * @param array $localizedLiterals An array with literals to use (as in $localizedLiterals property) + * @param array $parsedFormat An array describing format (as in $parsedFormats property) + * @param array $localizedLiterals An array with literals to use (as in $localizedLiterals property) * @return string Formatted date / time */ protected function doFormattingWithParsedFormat(\DateTimeInterface $dateTime, array $parsedFormat, array $localizedLiterals) @@ -191,7 +191,7 @@ protected function doFormattingWithParsedFormat(\DateTimeInterface $dateTime, ar * * @param \DateTimeInterface $dateTime PHP object representing particular point in time * @param string $subformat One element of format string (e.g., 'yyyy', 'mm', etc) - * @param array $localizedLiterals Array of date / time literals from CLDR + * @param array $localizedLiterals Array of date / time literals from CLDR * @return string Formatted part of date / time * @throws InvalidArgumentException When $subformat use symbol that is not recognized * @see \Neos\Flow\I18n\Cldr\Reader\DatesReader @@ -210,25 +210,25 @@ protected function doFormattingForSubpattern(\DateTimeInterface $dateTime, $subf if ($hour === 12) { $hour = 0; } - return $this->padString($hour, $formatLengthOfSubformat); + return $this->padString((string)$hour, $formatLengthOfSubformat); case 'k': $hour = (int)($dateTime->format('G')); if ($hour === 0) { $hour = 24; } - return $this->padString($hour, $formatLengthOfSubformat); + return $this->padString((string)$hour, $formatLengthOfSubformat); case 'a': return $localizedLiterals['dayPeriods']['format']['wide'][$dateTime->format('a')]; case 'm': - return $this->padString((int)($dateTime->format('i')), $formatLengthOfSubformat); + return $this->padString($dateTime->format('i'), $formatLengthOfSubformat); case 's': - return $this->padString((int)($dateTime->format('s')), $formatLengthOfSubformat); + return $this->padString($dateTime->format('s'), $formatLengthOfSubformat); case 'S': - return (string)round($dateTime->format('u'), $formatLengthOfSubformat); + return (string)round((int)$dateTime->format('u'), $formatLengthOfSubformat); case 'd': return $this->padString($dateTime->format('j'), $formatLengthOfSubformat); case 'D': - return $this->padString(((int)$dateTime->format('z') + 1), $formatLengthOfSubformat); + return $this->padString((string)((int)$dateTime->format('z') + 1), $formatLengthOfSubformat); case 'F': return (string)(int)(((int)$dateTime->format('j') + 6) / 7); case 'M': @@ -236,7 +236,7 @@ protected function doFormattingForSubpattern(\DateTimeInterface $dateTime, $subf $month = (int)$dateTime->format('n'); $formatType = ($subformat[0] === 'L') ? 'stand-alone' : 'format'; if ($formatLengthOfSubformat <= 2) { - return $this->padString($month, $formatLengthOfSubformat); + return $this->padString((string)$month, $formatLengthOfSubformat); } elseif ($formatLengthOfSubformat === 3) { return $localizedLiterals['months'][$formatType]['abbreviated'][$month]; } elseif ($formatLengthOfSubformat === 4) { @@ -250,7 +250,7 @@ protected function doFormattingForSubpattern(\DateTimeInterface $dateTime, $subf if ($formatLengthOfSubformat === 2) { $year %= 100; } - return $this->padString($year, $formatLengthOfSubformat); + return $this->padString((string)$year, $formatLengthOfSubformat); case 'E': $day = strtolower($dateTime->format('D')); if ($formatLengthOfSubformat <= 3) { @@ -270,7 +270,7 @@ protected function doFormattingForSubpattern(\DateTimeInterface $dateTime, $subf $quarter = (int)($dateTime->format('n') / 3.1) + 1; $formatType = ($subformat[0] === 'q') ? 'stand-alone' : 'format'; if ($formatLengthOfSubformat <= 2) { - return $this->padString($quarter, $formatLengthOfSubformat); + return $this->padString((string)$quarter, $formatLengthOfSubformat); } elseif ($formatLengthOfSubformat === 3) { return $localizedLiterals['quarters'][$formatType]['abbreviated'][$quarter]; } else { diff --git a/Neos.Flow/Classes/I18n/Formatter/FormatterInterface.php b/Neos.Flow/Classes/I18n/Formatter/FormatterInterface.php index 608cce9ab3..a03ceee643 100644 --- a/Neos.Flow/Classes/I18n/Formatter/FormatterInterface.php +++ b/Neos.Flow/Classes/I18n/Formatter/FormatterInterface.php @@ -24,7 +24,7 @@ interface FormatterInterface * * @param mixed $value Formatter-specific variable to format (can be integer, \DateTime, etc) * @param Locale $locale Locale to use - * @param array $styleProperties Integer-indexed array of formatter-specific style properties (can be empty) + * @param array $styleProperties Integer-indexed array of formatter-specific style properties (can be empty) * @return string String representation of $value provided, or (string)$value * @api */ diff --git a/Neos.Flow/Classes/I18n/Formatter/NumberFormatter.php b/Neos.Flow/Classes/I18n/Formatter/NumberFormatter.php index a70ea6688e..69a411f1df 100644 --- a/Neos.Flow/Classes/I18n/Formatter/NumberFormatter.php +++ b/Neos.Flow/Classes/I18n/Formatter/NumberFormatter.php @@ -58,7 +58,7 @@ public function injectCurrencyReader(CurrencyReader $currencyReader) * * @param mixed $value Formatter-specific variable to format (can be integer, \DateTime, etc) * @param Locale $locale Locale to use - * @param array $styleProperties Integer-indexed array of formatter-specific style properties (can be empty) + * @param array $styleProperties Integer-indexed array of formatter-specific style properties (can be empty) * @return string String representation of $value provided, or (string)$value * @api */ @@ -181,12 +181,12 @@ public function formatCurrencyNumber($number, Locale $locale, $currency, $format * as defined in CLDR specification. * * @param mixed $number Float or int, can be negative, can be NaN or infinite - * @param array $parsedFormat An array describing format (as in $parsedFormats property) - * @param array $symbols An array with symbols to use (as in $localeSymbols property) + * @param array|false $parsedFormat An array describing format (as in $parsedFormats property) + * @param array $symbols An array with symbols to use (as in $localeSymbols property) * @param string $currency Currency symbol to be inserted into formatted number (if applicable) * @return string Formatted number. Will return string-casted version of $number if pattern is false */ - protected function doFormattingWithParsedFormat($number, array $parsedFormat, array $symbols, $currency = null) + protected function doFormattingWithParsedFormat($number, array|false $parsedFormat, array $symbols, $currency = null) { if ($parsedFormat === false) { return (string)$number; @@ -232,8 +232,8 @@ protected function doFormattingWithParsedFormat($number, array $parsedFormat, ar $integerPart = str_pad($integerPart, $parsedFormat['minIntegerDigits'], '0', STR_PAD_LEFT); if ($parsedFormat['primaryGroupingSize'] > 0 && strlen($integerPart) > $parsedFormat['primaryGroupingSize']) { - $primaryGroupOfIntegerPart = substr($integerPart, - $parsedFormat['primaryGroupingSize']); - $restOfIntegerPart = substr($integerPart, 0, - $parsedFormat['primaryGroupingSize']); + $primaryGroupOfIntegerPart = substr($integerPart, - (int)$parsedFormat['primaryGroupingSize']); + $restOfIntegerPart = substr($integerPart, 0, - (int)$parsedFormat['primaryGroupingSize']); // Pad the numbers with spaces from the left, so the length of the string is a multiply of secondaryGroupingSize (and str_split() can split on equal parts) $padLengthToGetEvenSize = (int)((strlen($restOfIntegerPart) + $parsedFormat['secondaryGroupingSize'] - 1) / $parsedFormat['secondaryGroupingSize']) * $parsedFormat['secondaryGroupingSize']; diff --git a/Neos.Flow/Classes/I18n/Locale.php b/Neos.Flow/Classes/I18n/Locale.php index 8f67d4371b..11b7a237fc 100644 --- a/Neos.Flow/Classes/I18n/Locale.php +++ b/Neos.Flow/Classes/I18n/Locale.php @@ -89,11 +89,8 @@ class Locale * @throws Exception\InvalidLocaleIdentifierException If the locale identifier is not valid * @api */ - public function __construct($localeIdentifier) + public function __construct(string $localeIdentifier) { - if (!is_string($localeIdentifier)) { - throw new \InvalidArgumentException('A locale identifier must be of type string, ' . gettype($localeIdentifier) . ' given.', 1221216120); - } if (preg_match(self::PATTERN_MATCH_LOCALEIDENTIFIER, $localeIdentifier, $matches) !== 1) { throw new Exception\InvalidLocaleIdentifierException('"' . $localeIdentifier . '" is not a valid locale identifier.', 1221137814); } diff --git a/Neos.Flow/Classes/I18n/LocaleTypeConverter.php b/Neos.Flow/Classes/I18n/LocaleTypeConverter.php index 7c938a0126..0df231151f 100644 --- a/Neos.Flow/Classes/I18n/LocaleTypeConverter.php +++ b/Neos.Flow/Classes/I18n/LocaleTypeConverter.php @@ -43,7 +43,7 @@ class LocaleTypeConverter extends AbstractTypeConverter * * @param string $source the locale string * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface $configuration * @return Locale * @api diff --git a/Neos.Flow/Classes/I18n/Parser/DatetimeParser.php b/Neos.Flow/Classes/I18n/Parser/DatetimeParser.php index 7b3a6fad52..0c2d3a4aa8 100644 --- a/Neos.Flow/Classes/I18n/Parser/DatetimeParser.php +++ b/Neos.Flow/Classes/I18n/Parser/DatetimeParser.php @@ -127,10 +127,10 @@ public function parseDateAndTime($dateAndTimeToParse, I18n\Locale $locale, $form * Parses date and / or time using parsed format, in strict or lenient mode. * * @param string $datetimeToParse Date/time to be parsed - * @param array $parsedFormat Parsed format (from DatesReader) - * @param array $localizedLiterals Array of date / time literals from CLDR + * @param array $parsedFormat Parsed format (from DatesReader) + * @param array $localizedLiterals Array of date / time literals from CLDR * @param boolean $strictMode Work mode (strict when true, lenient when false) - * @return mixed Array of parsed date and / or time elements, false on failure + * @return array|false Array of parsed date and / or time elements, false on failure */ protected function doParsingWithParsedFormat($datetimeToParse, array $parsedFormat, array $localizedLiterals, $strictMode) { @@ -141,9 +141,9 @@ protected function doParsingWithParsedFormat($datetimeToParse, array $parsedForm * Parses date and / or time in strict mode. * * @param string $datetimeToParse Date/time to be parsed - * @param array $parsedFormat Format parsed by DatesReader - * @param array $localizedLiterals Array of date / time literals from CLDR - * @return array|false Array of parsed date and / or time elements, false on failure + * @param array $parsedFormat Format parsed by DatesReader + * @param array $localizedLiterals Array of date / time literals from CLDR + * @return array|false Array of parsed date and / or time elements, false on failure * @throws InvalidArgumentException When unexpected symbol found in format * @see DatesReader */ @@ -341,10 +341,9 @@ protected function doParsingInStrictMode($datetimeToParse, array $parsedFormat, * - some format fallback substitutions can be done (eg. 'Jan' for 'January') * * @param string $datetimeToParse Date/time to be parsed - * @param array $parsedFormat Format parsed by DatesReader - * @param array $localizedLiterals Array of date / time literals from CLDR - * @return array Array of parsed date and / or time elements (can be array of NULLs if nothing was parsed) - * @throws Exception\InvalidParseStringException + * @param array $parsedFormat Format parsed by DatesReader + * @param array $localizedLiterals Array of date / time literals from CLDR + * @return array Array of parsed date and / or time elements (can be array of NULLs if nothing was parsed) * @throws InvalidArgumentException When unexpected symbol found in format * @see DatesReader */ @@ -403,7 +402,7 @@ protected function doParsingInLenientMode($datetimeToParse, array $parsedFormat, $hour = $this->extractNumberAndGetPosition($datetimeToParse, $position); } if ($hour >= 0 && $hour <= 23) { - $numberOfCharactersToRemove = $position + strlen($hour); + $numberOfCharactersToRemove = $position + strlen((string)$hour); $datetimeElements['hour'] = (int)$hour; break; } @@ -533,9 +532,10 @@ protected function doParsingInLenientMode($datetimeToParse, array $parsedFormat, } } - $timezone = $matches[0]; + /** @var string $timezone */ + $timezone = $matches[0] ?? null; $numberOfCharactersToRemove = strpos($datetimeToParse, $timezone) + strlen($timezone); - $datetimeElements['timezone'] = $matches[0]; + $datetimeElements['timezone'] = $timezone; break; case 'D': case 'F': diff --git a/Neos.Flow/Classes/I18n/Parser/NumberParser.php b/Neos.Flow/Classes/I18n/Parser/NumberParser.php index dae3ef6be1..fee40f5298 100644 --- a/Neos.Flow/Classes/I18n/Parser/NumberParser.php +++ b/Neos.Flow/Classes/I18n/Parser/NumberParser.php @@ -66,7 +66,12 @@ public function injectNumbersReader(NumbersReader $numbersReader) */ public function parseNumberWithCustomPattern($numberToParse, $format, Locale $locale, $strictMode = true) { - return $this->doParsingWithParsedFormat($numberToParse, $this->numbersReader->parseCustomFormat($format), $this->numbersReader->getLocalizedSymbolsForLocale($locale), $strictMode); + return $this->doParsingWithParsedFormat( + $numberToParse, + $this->numbersReader->parseCustomFormat($format), + $this->numbersReader->getLocalizedSymbolsForLocale($locale), + $strictMode + ); } /** @@ -82,7 +87,16 @@ public function parseNumberWithCustomPattern($numberToParse, $format, Locale $lo public function parseDecimalNumber($numberToParse, Locale $locale, $formatLength = NumbersReader::FORMAT_LENGTH_DEFAULT, $strictMode = true) { NumbersReader::validateFormatLength($formatLength); - return $this->doParsingWithParsedFormat($numberToParse, $this->numbersReader->parseFormatFromCldr($locale, NumbersReader::FORMAT_TYPE_DECIMAL, $formatLength), $this->numbersReader->getLocalizedSymbolsForLocale($locale), $strictMode); + return $this->doParsingWithParsedFormat( + $numberToParse, + $this->numbersReader->parseFormatFromCldr( + $locale, + NumbersReader::FORMAT_TYPE_DECIMAL, + $formatLength + ), + $this->numbersReader->getLocalizedSymbolsForLocale($locale), + $strictMode + ); } /** @@ -98,15 +112,24 @@ public function parseDecimalNumber($numberToParse, Locale $locale, $formatLength public function parsePercentNumber($numberToParse, Locale $locale, $formatLength = NumbersReader::FORMAT_LENGTH_DEFAULT, $strictMode = true) { NumbersReader::validateFormatLength($formatLength); - return $this->doParsingWithParsedFormat($numberToParse, $this->numbersReader->parseFormatFromCldr($locale, NumbersReader::FORMAT_TYPE_PERCENT, $formatLength), $this->numbersReader->getLocalizedSymbolsForLocale($locale), $strictMode); + return $this->doParsingWithParsedFormat( + $numberToParse, + $this->numbersReader->parseFormatFromCldr( + $locale, + NumbersReader::FORMAT_TYPE_PERCENT, + $formatLength + ), + $this->numbersReader->getLocalizedSymbolsForLocale($locale), + $strictMode + ); } /** * Parses number using parsed format, in strict or lenient mode. * * @param string $numberToParse Number to be parsed - * @param array $parsedFormat Parsed format (from NumbersReader) - * @param array $localizedSymbols An array with symbols to use + * @param array{negativePrefix?: string, negativeSuffix?: string, multiplier: int} $parsedFormat Parsed format (from NumbersReader) + * @param array{decimal: string} $localizedSymbols An array with symbols to use * @param boolean $strictMode Work mode (strict when true, lenient when false) * @return mixed Parsed float number or false on failure */ @@ -122,8 +145,8 @@ protected function doParsingWithParsedFormat($numberToParse, array $parsedFormat * and if any of them is not fullfiled, parsing fails (false is returned). * * @param string $numberToParse Number to be parsed - * @param array $parsedFormat Parsed format (from NumbersReader) - * @param array $localizedSymbols An array with symbols to use + * @param array $parsedFormat Parsed format (from NumbersReader) + * @param array $localizedSymbols An array with symbols to use * @return mixed Parsed float number or false on failure */ protected function doParsingInStrictMode($numberToParse, array $parsedFormat, array $localizedSymbols) @@ -184,7 +207,7 @@ protected function doParsingInStrictMode($numberToParse, array $parsedFormat, ar $numberToParse = str_replace([$localizedSymbols['group'], $localizedSymbols['decimal']], ['', '.'], $numberToParse); - $positionOfDecimalSeparator = strpos($numberToParse, '.'); + $positionOfDecimalSeparator = strpos($numberToParse, '.') ?: 0; $integerPart = substr($numberToParse, 0, $positionOfDecimalSeparator); $decimalPart = substr($numberToParse, $positionOfDecimalSeparator + 1); } @@ -239,8 +262,8 @@ protected function doParsingInStrictMode($numberToParse, array $parsedFormat, ar * 7. Try to match negative suffix after last digit * * @param string $numberToParse Number to be parsed - * @param array $parsedFormat Parsed format (from NumbersReader) - * @param array $localizedSymbols An array with symbols to use + * @param array{negativePrefix?: string, negativeSuffix?: string, multiplier: int} $parsedFormat Parsed format (from NumbersReader) + * @param array{decimal: string} $localizedSymbols An array with symbols to use * @return mixed Parsed float number or false on failure */ protected function doParsingInLenientMode($numberToParse, array $parsedFormat, array $localizedSymbols) diff --git a/Neos.Flow/Classes/I18n/Service.php b/Neos.Flow/Classes/I18n/Service.php index c4aa342a3e..a00fc841a9 100644 --- a/Neos.Flow/Classes/I18n/Service.php +++ b/Neos.Flow/Classes/I18n/Service.php @@ -27,7 +27,7 @@ class Service { /** - * @var array + * @var array */ protected $settings; @@ -65,7 +65,7 @@ class Service protected $localeBasePath = 'resource://'; /** - * @param array $settings + * @param array $settings * @return void */ public function injectSettings(array $settings) @@ -120,9 +120,9 @@ public function getConfiguration() * any change. * * @param string $pathAndFilename Path to the file - * @param Locale $locale Desired locale of localized file + * @param ?Locale $locale Desired locale of localized file * @param boolean $strict Whether to match only provided locale (true) or search for best-matching locale (false) - * @return array Path to the localized file (or $filename when no localized file was found) and the matched locale + * @return array{0: string, 1: Locale} Path to the localized file (or $filename when no localized file was found) and the matched locale * @see Configuration::setFallbackRule() * @api */ @@ -134,7 +134,7 @@ public function getLocalizedFilename($pathAndFilename, ?Locale $locale = null, $ $filename = basename($pathAndFilename); if ((strpos($filename, '.')) !== false) { - $dotPosition = strrpos($pathAndFilename, '.'); + $dotPosition = strrpos($pathAndFilename, '.') ?: 0; $pathAndFilenameWithoutExtension = substr($pathAndFilename, 0, $dotPosition); $extension = substr($pathAndFilename, $dotPosition); } else { @@ -175,7 +175,7 @@ public function getLocalizedFilename($pathAndFilename, ?Locale $locale = null, $ * @param string $path Base directory to the translation files * @param string $sourceName name of the translation source * @param Locale $locale Desired locale of XLIFF file - * @return array Path to the localized file (or $filename when no localized file was found) and the matched locale + * @return array{0: string|false, 1: Locale} Path to the localized file (or $filename when no localized file was found) and the matched locale * @see Configuration::setFallbackRule() * @api */ @@ -198,7 +198,7 @@ public function getXliffFilenameAndPath($path, $sourceName, ?Locale $locale = nu * Build a chain of locale objects according to the fallback rule and * the available locales. * @param Locale $locale - * @return array + * @return array */ public function getLocaleChain(Locale $locale) { diff --git a/Neos.Flow/Classes/I18n/Translator.php b/Neos.Flow/Classes/I18n/Translator.php index c4938afddd..94bd7be90b 100644 --- a/Neos.Flow/Classes/I18n/Translator.php +++ b/Neos.Flow/Classes/I18n/Translator.php @@ -113,7 +113,7 @@ public function injectPluralsReader(Cldr\Reader\PluralsReader $pluralsReader) * If no $locale is provided, default system locale will be used. * * @param string $originalLabel Untranslated message - * @param array $arguments An array of values to replace placeholders with + * @param array $arguments An array of values to replace placeholders with * @param mixed $quantity A number to find plural form for (float or int), NULL to not use plural forms * @param Locale $locale Locale to use (NULL for default one) * @param string $sourceName Name of file with translations, base path is $packageKey/Resources/Private/Locale/Translations/ @@ -170,7 +170,7 @@ public function translateByOriginalLabel($originalLabel, array $arguments = [], * be chosen and used to choose correct translation variant. * * @param string $labelId Key to use for finding translation - * @param array $arguments An array of values to replace placeholders with + * @param array $arguments An array of values to replace placeholders with * @param mixed $quantity A number to find plural form for (float or int), NULL to not use plural forms * @param Locale $locale Locale to use (NULL for default one) * @param string $sourceName Name of file with translations, base path is $packageKey/Resources/Private/Locale/Translations/ diff --git a/Neos.Flow/Classes/I18n/Utility.php b/Neos.Flow/Classes/I18n/Utility.php index be8ada326b..33ba019a42 100644 --- a/Neos.Flow/Classes/I18n/Utility.php +++ b/Neos.Flow/Classes/I18n/Utility.php @@ -123,7 +123,9 @@ public static function extractLocaleTagFromDirectory($directory) $directoryParts = explode('/', rtrim($directory, '/')); $lastDirectoryPart = array_pop($directoryParts); - if ($lastDirectoryPart !== null && preg_match(Locale::PATTERN_MATCH_LOCALEIDENTIFIER, $lastDirectoryPart) === 1) { + if ( + preg_match(Locale::PATTERN_MATCH_LOCALEIDENTIFIER, $lastDirectoryPart) === 1 + ) { return $lastDirectoryPart; } diff --git a/Neos.Flow/Classes/I18n/Xliff/Model/FileAdapter.php b/Neos.Flow/Classes/I18n/Xliff/Model/FileAdapter.php index ce650b0ead..1ea3d0fea6 100644 --- a/Neos.Flow/Classes/I18n/Xliff/Model/FileAdapter.php +++ b/Neos.Flow/Classes/I18n/Xliff/Model/FileAdapter.php @@ -29,6 +29,8 @@ * numbers. Leaving it this way in .xlf files, makes it possible to easily convert * them to .po (e.g. using xliff2po from Translation Toolkit), edit with Poedit, * and convert back to .xlf without any information loss (using po2xliff). + * + * @phpstan-type XliffRecord array{fileIdentifier: string, sourceLocale?: Locale, translationUnits?: array>} */ class FileAdapter { @@ -39,9 +41,9 @@ class FileAdapter protected $i18nLogger; /** - * @var array + * @var XliffRecord */ - protected $fileData = []; + protected array $fileData; /** * @var Locale @@ -50,7 +52,7 @@ class FileAdapter /** - * @param array $fileData + * @param XliffRecord $fileData * @param Locale $requestedLocale */ public function __construct(array $fileData, Locale $requestedLocale) @@ -69,7 +71,7 @@ public function __construct(array $fileData, Locale $requestedLocale) * * @param string $source Label in original language ("source" tag in XLIFF) * @param integer $pluralFormIndex Index of plural form to use (starts with 0) - * @return mixed Translated label or false on failure + * @return string|false Translated label or false on failure */ public function getTargetBySource($source, $pluralFormIndex = 0) { @@ -117,21 +119,14 @@ public function getTargetByTransUnitId($transUnitId, $pluralFormIndex = 0) return false; } - if (!isset($this->fileData['translationUnits'][$transUnitId][$pluralFormIndex]['target'])) { - $this->i18nLogger->log( - 'The target translation was empty for the trans-unit element with the id "' . $transUnitId . '" and the plural form index "' . $pluralFormIndex . '" in ' . $this->fileData['fileIdentifier'], - LOG_DEBUG - ); - } - return $this->fileData['translationUnits'][$transUnitId][$pluralFormIndex]['target']; } /** - * @return array + * @return array> */ public function getTranslationUnits(): array { - return $this->fileData['translationUnits']; + return $this->fileData['translationUnits'] ?? []; } } diff --git a/Neos.Flow/Classes/I18n/Xliff/Service/XliffFileProvider.php b/Neos.Flow/Classes/I18n/Xliff/Service/XliffFileProvider.php index 771f57bdbd..9a35c94174 100644 --- a/Neos.Flow/Classes/I18n/Xliff/Service/XliffFileProvider.php +++ b/Neos.Flow/Classes/I18n/Xliff/Service/XliffFileProvider.php @@ -25,6 +25,7 @@ * A provider service for XLIFF file objects within the application * * @Flow\Scope("singleton") + * @phpstan-import-type XliffRecord from FileAdapter */ class XliffFileProvider { @@ -65,7 +66,7 @@ class XliffFileProvider protected $xliffBasePath = 'Private/Translations/'; /** - * @var array + * @var array> */ protected $files = []; @@ -94,7 +95,7 @@ public function initializeObject() /** * @param string $fileId * @param Locale $locale - * @return array + * @return XliffRecord * @todo Add XLIFF 2.0 support */ public function getMergedFileData($fileId, Locale $locale): array @@ -126,7 +127,7 @@ public function getMergedFileData($fileId, Locale $locale): array /** * @param string $translationPath - * @param array $parsedData + * @param array{fileIdentifier: string, sourceLocale?: Locale, translationUnits?: array>} $parsedData * @param string $fileId * @param string $defaultPackageName * @return void @@ -135,7 +136,7 @@ protected function readDirectoryRecursively(string $translationPath, array & $pa { foreach (Files::readDirectoryRecursively($translationPath) as $filePath) { $defaultSource = trim(str_replace($translationPath, '', $filePath), '/'); - $defaultSource = substr($defaultSource, 0, strrpos($defaultSource, '.')); + $defaultSource = substr($defaultSource, 0, strrpos($defaultSource, '.') ?: 0); $relevantOffset = null; $documentVersion = null; @@ -161,7 +162,8 @@ function (\XMLReader $file, $offset, $version) use ($fileId, &$documentVersion, $xliffParser = $this->getParser($documentVersion); if ($xliffParser) { $fileData = $xliffParser->getFileDataFromDocument($filePath, $relevantOffset); - $parsedData = Arrays::arrayMergeRecursiveOverrule($parsedData, $fileData); + /** @phpstan-ignore parameterByRef.type (arrayMergeRecursiveOverrule will not reduce the array shape) */ + $parsedData = Arrays::arrayMergeRecursiveOverrule($parsedData, $fileData ?: []); } } } @@ -186,7 +188,6 @@ public function getParser($documentVersion) switch ($documentVersion) { case '1.2': return new V12XliffParser(); - break; default: return null; } diff --git a/Neos.Flow/Classes/I18n/Xliff/V12/XliffParser.php b/Neos.Flow/Classes/I18n/Xliff/V12/XliffParser.php index 092686bbfb..30d18a4691 100644 --- a/Neos.Flow/Classes/I18n/Xliff/V12/XliffParser.php +++ b/Neos.Flow/Classes/I18n/Xliff/V12/XliffParser.php @@ -37,7 +37,7 @@ class XliffParser extends AbstractXmlParser * Returns array representation of XLIFF data, starting from a root node. * * @param \SimpleXMLElement $root A root node - * @return array An array representing parsed XLIFF + * @return array>}> An array representing parsed XLIFF * @throws InvalidXliffDataException * @todo Support "approved" attribute */ @@ -54,7 +54,7 @@ protected function doParsingFromRoot(\SimpleXMLElement $root): array /** * @param \SimpleXMLElement $file - * @return array + * @return array{sourceLocale: Locale, translationUnits?: array>} * @throws InvalidXliffDataException */ protected function getFileData(\SimpleXMLElement $file): array @@ -110,7 +110,7 @@ protected function getFileData(\SimpleXMLElement $file): array if (!empty($parsedTranslationElement)) { if (isset($translationElement->{'trans-unit'}[0]['id'])) { $id = (string)$translationElement->{'trans-unit'}[0]['id']; - $id = substr($id, 0, strpos($id, '[')); + $id = substr($id, 0, strpos($id, '[') ?: 0); } else { throw new InvalidXliffDataException('A trans-unit tag without id attribute was found, validate your XLIFF files.', 1329399258); } @@ -128,7 +128,7 @@ protected function getFileData(\SimpleXMLElement $file): array /** * @param string $sourcePath * @param mixed $fileOffset - * @return array|null + * @return array{sourceLocale: Locale, translationUnits?: array>}|null * @throws InvalidXliffDataException * @throws InvalidXmlFileException */ diff --git a/Neos.Flow/Classes/Log/PsrLoggerFactory.php b/Neos.Flow/Classes/Log/PsrLoggerFactory.php index 5825f0300c..0d473677a1 100644 --- a/Neos.Flow/Classes/Log/PsrLoggerFactory.php +++ b/Neos.Flow/Classes/Log/PsrLoggerFactory.php @@ -23,14 +23,14 @@ class PsrLoggerFactory implements PsrLoggerFactoryInterface protected $instances = []; /** - * @var array + * @var array */ protected $configuration = []; /** * PsrLoggerFactory constructor. * - * @param array $configuration + * @param array $configuration */ public function __construct(array $configuration = []) { @@ -69,7 +69,7 @@ public function get(string $identifier): \Psr\Log\LoggerInterface /** * Create a new instance of this PsrLoggerFactory * - * @param array $configuration + * @param array $configuration * @return PsrLoggerFactory * @api */ @@ -81,7 +81,7 @@ public static function create(array $configuration): PsrLoggerFactory /** * Instantiate all configured backends * - * @param array $configuration + * @param array $configuration * @return BackendInterface[] * @throws \Exception */ @@ -101,7 +101,7 @@ protected function instantiateBackends(array $configuration): array * Instantiate a backend based on configuration. * * @param string $class - * @param array $options + * @param array $options * @return BackendInterface * @throws \Exception */ diff --git a/Neos.Flow/Classes/Log/PsrLoggerFactoryInterface.php b/Neos.Flow/Classes/Log/PsrLoggerFactoryInterface.php index 2c16210fe9..6599925cab 100644 --- a/Neos.Flow/Classes/Log/PsrLoggerFactoryInterface.php +++ b/Neos.Flow/Classes/Log/PsrLoggerFactoryInterface.php @@ -30,7 +30,7 @@ public function get(string $identifier); * Reminder for super low level things like this, the Bootstrap class has * some static properties for you. * - * @param array $configuration + * @param array $configuration * @return static * @api */ diff --git a/Neos.Flow/Classes/Log/ThrowableStorage/FileStorage.php b/Neos.Flow/Classes/Log/ThrowableStorage/FileStorage.php index 4fe0cd53a4..4d28b91c6c 100644 --- a/Neos.Flow/Classes/Log/ThrowableStorage/FileStorage.php +++ b/Neos.Flow/Classes/Log/ThrowableStorage/FileStorage.php @@ -10,7 +10,6 @@ use Neos\Flow\Log\ThrowableStorageInterface; use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Utility\Files; -use Psr\Http\Message\RequestInterface; /** * Stores detailed information about throwables into files. @@ -53,7 +52,7 @@ class FileStorage implements ThrowableStorageInterface /** * Factory method to get an instance. * - * @param array $options + * @param array{storagePath?: string, maximumThrowableDumpAge?: int, maximumThrowableDumpCount: ?int} $options * @return ThrowableStorageInterface */ public static function createWithOptions(array $options): ThrowableStorageInterface @@ -88,14 +87,13 @@ public function __construct(string $storagePath, int $maximumThrowableDumpAge, i } $bootstrap = Bootstrap::$staticObjectManager->get(Bootstrap::class); - /* @var Bootstrap $bootstrap */ $requestHandler = $bootstrap->getActiveRequestHandler(); if (!$requestHandler instanceof HttpRequestHandlerInterface) { return $output; } $request = $requestHandler->getHttpRequest(); - $output .= PHP_EOL . 'HTTP REQUEST:' . PHP_EOL . ($request instanceof RequestInterface ? RequestInformationHelper::renderRequestInformation($request) : '[request was empty]') . PHP_EOL; + $output .= PHP_EOL . 'HTTP REQUEST:' . PHP_EOL . RequestInformationHelper::renderRequestInformation($request) . PHP_EOL; $output .= PHP_EOL . 'PHP PROCESS:' . PHP_EOL . 'Inode: ' . getmyinode() . PHP_EOL . 'PID: ' . getmypid() . PHP_EOL . 'UID: ' . getmyuid() . PHP_EOL . 'GID: ' . getmygid() . PHP_EOL . 'User: ' . get_current_user() . PHP_EOL; return $output; @@ -137,7 +135,7 @@ public function setBacktraceRenderer(\Closure $backtraceRenderer) * Exception # in of : - See also: * * @param \Throwable $throwable - * @param array $additionalData + * @param array $additionalData * @return string Informational message about the stored throwable */ public function logThrowable(\Throwable $throwable, array $additionalData = []) @@ -177,14 +175,14 @@ public function logThrowable(\Throwable $throwable, array $additionalData = []) protected function generateUniqueReferenceCode() { $timestamp = $_SERVER['REQUEST_TIME'] ?? time(); - return date('YmdHis', $timestamp) . substr(md5(rand()), 0, 6); + return date('YmdHis', $timestamp) . substr(md5((string)rand()), 0, 6); } /** * Get current error post mortem informations with support for error chaining * * @param \Throwable $error - * @param array $additionalData + * @param array $additionalData * @return string */ protected function renderErrorInfo(\Throwable $error, array $additionalData = []) @@ -226,7 +224,7 @@ protected function getErrorLogMessage(\Throwable $error) $errorCodeString = ' [' . $error->getCode() . ']'; } $backTrace = $error->getTrace(); - $line = isset($backTrace[0]['line']) ? ' in line ' . $backTrace[0]['line'] . ' of ' . $backTrace[0]['file'] : ''; + $line = isset($backTrace[0]['line']) ? ' in line ' . $backTrace[0]['line'] . ' of ' . ($backTrace[0]['file'] ?? 'unknown') : ''; return 'Exception' . $errorCodeString . $line . ': ' . $error->getMessage(); } @@ -234,7 +232,7 @@ protected function getErrorLogMessage(\Throwable $error) /** * Renders background information about the circumstances of the exception. * - * @param array $backtrace + * @param array $backtrace * @return string */ protected function renderBacktrace($backtrace) @@ -281,7 +279,7 @@ protected function cleanupThrowableDumps(): void if ($this->maximumThrowableDumpCount > 0) { // this returns alphabetically ordered, so oldest first, as we have a date/time in the name - $existingDumps = glob(Files::concatenatePaths([$this->storagePath, '*'])); + $existingDumps = glob(Files::concatenatePaths([$this->storagePath, '*'])) ?: []; $existingDumpsCount = count($existingDumps); if ($existingDumpsCount > $this->maximumThrowableDumpCount) { $dumpsToRemove = array_slice($existingDumps, 0, $existingDumpsCount - $this->maximumThrowableDumpCount); diff --git a/Neos.Flow/Classes/Log/ThrowableStorageInterface.php b/Neos.Flow/Classes/Log/ThrowableStorageInterface.php index 747d20e055..3fb6a2763d 100644 --- a/Neos.Flow/Classes/Log/ThrowableStorageInterface.php +++ b/Neos.Flow/Classes/Log/ThrowableStorageInterface.php @@ -13,7 +13,7 @@ interface ThrowableStorageInterface * * Note that throwable storages must work without proxy so all dependencies need to be resolved manually or via options. * - * @param array $options + * @param array $options * @return ThrowableStorageInterface */ public static function createWithOptions(array $options): ThrowableStorageInterface; @@ -25,7 +25,7 @@ public static function createWithOptions(array $options): ThrowableStorageInterf * can be logged or displayed as needed. * * @param \Throwable $throwable The throwable to log - * @param array $additionalData Additional data to log + * @param array $additionalData Additional data to log * @return string Informational message about the stored throwable * @api */ diff --git a/Neos.Flow/Classes/Log/Utility/LogEnvironment.php b/Neos.Flow/Classes/Log/Utility/LogEnvironment.php index 8d3dec8c52..38e19301a3 100644 --- a/Neos.Flow/Classes/Log/Utility/LogEnvironment.php +++ b/Neos.Flow/Classes/Log/Utility/LogEnvironment.php @@ -23,7 +23,7 @@ abstract class LogEnvironment { /** - * @var array + * @var array */ protected static $packageKeys = []; @@ -38,7 +38,7 @@ abstract class LogEnvironment * in an log method call. * * @param string $methodName - * @return array + * @return array */ public static function fromMethodName(string $methodName): array { @@ -62,9 +62,9 @@ public static function fromMethodName(string $methodName): array /** * @param string $className - * @return string + * @return ?string */ - protected static function getPackageKeyFromClassName(string $className): string + protected static function getPackageKeyFromClassName(string $className): ?string { $packageKeys = static::getPackageKeys(); $classPathArray = explode('\\', $className); @@ -86,7 +86,7 @@ protected static function getPackageKeyFromClassName(string $className): string } /** - * @return array + * @return array * @Flow\CompileStatic */ protected static function getPackageKeys(): array diff --git a/Neos.Flow/Classes/Monitor/ChangeDetectionStrategy/ModificationTimeStrategy.php b/Neos.Flow/Classes/Monitor/ChangeDetectionStrategy/ModificationTimeStrategy.php index 39a239c735..70f3e46746 100644 --- a/Neos.Flow/Classes/Monitor/ChangeDetectionStrategy/ModificationTimeStrategy.php +++ b/Neos.Flow/Classes/Monitor/ChangeDetectionStrategy/ModificationTimeStrategy.php @@ -31,7 +31,7 @@ class ModificationTimeStrategy implements ChangeDetectionStrategyInterface, Stra protected $cache; /** - * @var array + * @var array */ protected $filesAndModificationTimes = []; @@ -61,7 +61,7 @@ public function injectCache(StringFrontend $cache) public function setFileMonitor(FileMonitor $fileMonitor) { $this->fileMonitor = $fileMonitor; - $this->filesAndModificationTimes = json_decode($this->cache->get($this->fileMonitor->getIdentifier() . '_filesAndModificationTimes'), true); + $this->filesAndModificationTimes = json_decode($this->cache->get($this->fileMonitor->getIdentifier() . '_filesAndModificationTimes') ?: '', true); } /** @@ -120,7 +120,7 @@ public function setFileDeleted($pathAndFilename) public function shutdownObject() { if ($this->modificationTimesChanged === true) { - $this->cache->set($this->fileMonitor->getIdentifier() . '_filesAndModificationTimes', json_encode($this->filesAndModificationTimes)); + $this->cache->set($this->fileMonitor->getIdentifier() . '_filesAndModificationTimes', json_encode($this->filesAndModificationTimes, JSON_THROW_ON_ERROR)); } } } diff --git a/Neos.Flow/Classes/Monitor/FileMonitor.php b/Neos.Flow/Classes/Monitor/FileMonitor.php index 948e8ce772..883f37a753 100644 --- a/Neos.Flow/Classes/Monitor/FileMonitor.php +++ b/Neos.Flow/Classes/Monitor/FileMonitor.php @@ -55,33 +55,33 @@ class FileMonitor protected $cache; /** - * @var array + * @var array */ protected $monitoredFiles = []; /** - * @var array + * @var array */ protected $monitoredDirectories = []; /** * Changed files for this monitor * - * @var array + * @var array|null */ protected $changedFiles = null; /** * The changed paths for this monitor * - * @var array + * @var array|null */ protected $changedPaths = null; /** * Array of directories and files that were cached on the last run. * - * @var array|null + * @var array>|null */ protected $directoriesAndFiles = null; @@ -106,6 +106,9 @@ public function __construct($identifier) public static function createFileMonitorAtBoot($identifier, Bootstrap $bootstrap) { $fileMonitorCache = $bootstrap->getEarlyInstance(CacheManager::class)->getCache('Flow_Monitor'); + if (!$fileMonitorCache instanceof StringFrontend) { + throw new \Exception('The file monitor cache must have a string frontend', 1744387394); + } // The change detector needs to be instantiated and registered manually because // it has a complex dependency (cache) but still needs to be a singleton. @@ -135,7 +138,7 @@ public function injectChangeDetectionStrategy(ChangeDetectionStrategyInterface $ } /** - * Injects the Singal Slot Dispatcher because classes of the Monitor subpackage cannot be proxied by the AOP + * Injects the Signal Slot Dispatcher because classes of the Monitor subpackage cannot be proxied by the AOP * framework because it is not initialized at the time the monitoring is used. * * @param Dispatcher $signalDispatcher The Signal Slot Dispatcher @@ -187,11 +190,8 @@ public function getIdentifier() * @throws \InvalidArgumentException * @api */ - public function monitorFile($pathAndFilename) + public function monitorFile(string $pathAndFilename) { - if (!is_string($pathAndFilename)) { - throw new \InvalidArgumentException('String expected, ' . gettype($pathAndFilename), ' given.', 1231171809); - } $pathAndFilename = Files::getUnixStylePath($pathAndFilename); if (array_search($pathAndFilename, $this->monitoredFiles) === false) { $this->monitoredFiles[] = $pathAndFilename; @@ -203,16 +203,13 @@ public function monitorFile($pathAndFilename) * All files in these directories will be monitored too. * * @param string $path Absolute path of the directory to monitor - * @param string $filenamePattern A pattern for filenames to consider for file monitoring (regular expression) + * @param ?string $filenamePattern A pattern for filenames to consider for file monitoring (regular expression) * @return void * @throws \InvalidArgumentException * @api */ - public function monitorDirectory($path, $filenamePattern = null) + public function monitorDirectory(string $path, $filenamePattern = null) { - if (!is_string($path)) { - throw new \InvalidArgumentException('String expected, ' . gettype($path), ' given.', 1231171810); - } $path = Files::getNormalizedPath(Files::getUnixStylePath($path)); if (!array_key_exists($path, $this->monitoredDirectories)) { $this->monitoredDirectories[$path] = $filenamePattern; @@ -222,7 +219,7 @@ public function monitorDirectory($path, $filenamePattern = null) /** * Returns a list of all monitored files * - * @return array A list of paths and filenames of monitored files + * @return array A list of paths and filenames of monitored files * @api */ public function getMonitoredFiles() @@ -233,7 +230,7 @@ public function getMonitoredFiles() /** * Returns a list of all monitored directories * - * @return array A list of paths of monitored directories + * @return array A list of paths of monitored directories * @api */ public function getMonitoredDirectories() @@ -265,14 +262,17 @@ public function detectChanges() } $this->directoriesAndFiles = null; } - + /** @phpstan-ignore argument.type (never null) */ $changedFileCount = count($this->changedFiles); + /** @phpstan-ignore argument.type (never null) */ $changedPathCount = count($this->changedPaths); if ($changedFileCount > 0) { + /** @phpstan-ignore argument.type (never null) */ $this->emitFilesHaveChanged($this->identifier, $this->changedFiles); } if ($changedPathCount > 0) { + /** @phpstan-ignore argument.type (never null) */ $this->emitDirectoriesHaveChanged($this->identifier, $this->changedPaths); } if ($changedFileCount > 0 || $changedPathCount) { @@ -284,7 +284,7 @@ public function detectChanges() * Detect changes for one of the monitored paths. * * @param string $path - * @param string $filenamePattern + * @param ?string $filenamePattern * @return boolean true if any changes were detected in this path */ protected function detectChangesOnPath($path, $filenamePattern) @@ -341,7 +341,7 @@ protected function detectChangesOnPath($path, $filenamePattern) * Read a monitored directory recursively, taking into account filename patterns * * @param string $path The path of a monitored directory - * @param string $filenamePattern + * @param ?string $filenamePattern * @return \Generator A generator returning filenames with full path */ protected function readMonitoredDirectoryRecursively($path, $filenamePattern) @@ -377,7 +377,7 @@ protected function readMonitoredDirectoryRecursively($path, $filenamePattern) protected function loadDetectedDirectoriesAndFiles() { if ($this->directoriesAndFiles === null) { - $this->directoriesAndFiles = json_decode($this->cache->get($this->identifier . '_directoriesAndFiles'), true); + $this->directoriesAndFiles = json_decode($this->cache->get($this->identifier . '_directoriesAndFiles') ?: '', true); if (!is_array($this->directoriesAndFiles)) { $this->directoriesAndFiles = []; } @@ -391,12 +391,12 @@ protected function loadDetectedDirectoriesAndFiles() */ protected function saveDetectedDirectoriesAndFiles() { - $this->cache->set($this->identifier . '_directoriesAndFiles', json_encode($this->directoriesAndFiles)); + $this->cache->set($this->identifier . '_directoriesAndFiles', json_encode($this->directoriesAndFiles, JSON_THROW_ON_ERROR)); } /** * @param string $path - * @param array $files + * @param array $files * @return void */ protected function setDetectedFilesForPath($path, array $files) @@ -407,8 +407,8 @@ protected function setDetectedFilesForPath($path, array $files) /** * Detects changes in the given list of files and emits signals if necessary. * - * @param array $pathAndFilenames A list of full path and filenames of files to check - * @return array An array of changed files (key = path and filenmae) and their status (value) + * @param array $pathAndFilenames A list of full path and filenames of files to check + * @return array An array of changed files (key = path and filenmae) and their status (value) */ protected function detectChangedFiles(array $pathAndFilenames) { @@ -426,7 +426,7 @@ protected function detectChangedFiles(array $pathAndFilenames) * Signalizes that the specified file has changed * * @param string $monitorIdentifier Name of the monitor which detected the change - * @param array $changedFiles An array of changed files (key = path and filename) and their status (value) + * @param array $changedFiles An array of changed files (key = path and filename) and their status (value) * @return void * @api */ @@ -439,7 +439,7 @@ protected function emitFilesHaveChanged($monitorIdentifier, array $changedFiles) * Signalizes that the specified directory has changed * * @param string $monitorIdentifier Name of the monitor which detected the change - * @param array $changedDirectories An array of changed directories (key = path) and their status (value) + * @param array $changedDirectories An array of changed directories (key = path) and their status (value) * @return void * @api */ diff --git a/Neos.Flow/Classes/Mvc/ActionRequest.php b/Neos.Flow/Classes/Mvc/ActionRequest.php index d3fb98ff25..fb62ef4972 100644 --- a/Neos.Flow/Classes/Mvc/ActionRequest.php +++ b/Neos.Flow/Classes/Mvc/ActionRequest.php @@ -72,7 +72,7 @@ class ActionRequest /** * The arguments for this request. They must be only simple types, no * objects allowed. - * @var array + * @var array */ protected $arguments = []; @@ -81,14 +81,14 @@ class ActionRequest * All framework-internal arguments start with double underscore (__), * and are only used from within the framework. Not for user consumption. * Internal Arguments can be objects, in contrast to public arguments. - * @var array + * @var array */ protected $internalArguments = []; /** * Arguments and configuration for plugins – including widgets – which are * sub controllers to the controller referred to by this request. - * @var array + * @var array */ protected $pluginArguments = []; @@ -158,15 +158,10 @@ public function createSubRequest(): ActionRequest /** * Returns the parent request * - * @return ActionRequest * @api */ public function getParentRequest(): ?ActionRequest { - if ($this->isMainRequest()) { - return null; - } - return $this->parentRequest; } @@ -205,7 +200,7 @@ public function getMainRequest(): ActionRequest * Checks if this request is the uppermost ActionRequest, just one below the * HTTP request. * - * @phpstan-assert-if-true null $this->getParentRequest() + * @phpstan-assert-if-true null $this->parentRequest * @return boolean * @api */ @@ -300,10 +295,14 @@ public function setControllerObjectName(string $unknownCasedControllerObjectName throw new UnknownObjectException('The object "' . $unknownCasedControllerObjectName . '" is not registered.', 1268844071); } - $this->controllerPackageKey = $this->objectManager->getPackageKeyByObjectName($controllerObjectName); + $controllerPackageKey = $this->objectManager->getPackageKeyByObjectName($controllerObjectName); + if (!$controllerPackageKey) { + throw new \Exception('Failed to resolve package key for controller ' . $controllerObjectName, 1744329205); + } + $this->controllerPackageKey = $controllerPackageKey; $matches = []; - $subject = substr($controllerObjectName, strlen($this->controllerPackageKey) + 1); + $subject = substr($controllerObjectName, strlen($controllerPackageKey) + 1); preg_match( '/ ^( @@ -318,7 +317,7 @@ public function setControllerObjectName(string $unknownCasedControllerObjectName ); $this->controllerSubpackageKey = $matches['subpackageKey'] ?? null; - $this->controllerName = $matches['controllerName']; + $this->controllerName = $matches['controllerName'] ?? ''; } /** @@ -444,6 +443,9 @@ public function getControllerActionName(): string $controllerObjectName = $this->getControllerObjectName(); if ($controllerObjectName !== '' && ($this->controllerActionName === strtolower($this->controllerActionName))) { $controllerClassName = $this->objectManager->getClassNameByObjectName($controllerObjectName); + if ($controllerClassName === false) { + throw new \Exception('Could not resolve controller class for ' . $controllerObjectName); + } $lowercaseActionMethodName = $this->controllerActionName . 'action'; foreach (get_class_methods($controllerClassName) as $existingMethodName) { if (strtolower($existingMethodName) === $lowercaseActionMethodName) { @@ -512,7 +514,7 @@ public function setArgument(string $argumentName, $value): void * Returns the value of the specified argument * * @param string $argumentName Name of the argument - * @return string|array Value of the argument + * @return string|array Value of the argument * @throws Exception\NoSuchArgumentException if such an argument does not exist * @api */ @@ -542,7 +544,7 @@ public function hasArgument(string $argumentName): bool * The arguments array will be reset therefore any arguments * which existed before will be overwritten! * - * @param array $arguments An array of argument names and their values + * @param array $arguments An array of argument names and their values * @return void * @throws Exception\InvalidArgumentNameException if an argument name is not a string * @throws Exception\InvalidArgumentTypeException if an argument value is an object @@ -560,7 +562,7 @@ public function setArguments(array $arguments): void /** * Returns an Array of arguments and their values * - * @return array Array of arguments and their values (which may be arguments and values as well) + * @return array Array of arguments and their values (which may be arguments and values as well) * @api */ public function getArguments(): array @@ -586,7 +588,7 @@ public function getInternalArgument(string $argumentName) * Returns the internal arguments of the request, that is, all arguments whose * name starts with two underscores. * - * @return array + * @return array */ public function getInternalArguments(): array { @@ -621,7 +623,7 @@ public function getArgumentNamespace(): string /** * Returns an array of plugin argument configurations * - * @return array + * @return array */ public function getPluginArguments(): array { diff --git a/Neos.Flow/Classes/Mvc/ActionRequestFactory.php b/Neos.Flow/Classes/Mvc/ActionRequestFactory.php index e09d7eda10..134b658836 100644 --- a/Neos.Flow/Classes/Mvc/ActionRequestFactory.php +++ b/Neos.Flow/Classes/Mvc/ActionRequestFactory.php @@ -12,7 +12,7 @@ class ActionRequestFactory { /** * @param ServerRequestInterface $httpRequest - * @param array $additionalArguments + * @param array $additionalArguments * @return ActionRequest * @throws Exception\InvalidActionNameException * @throws Exception\InvalidArgumentNameException @@ -33,7 +33,7 @@ public function createActionRequest(ServerRequestInterface $httpRequest, array $ /** * @param ServerRequestInterface $httpRequest - * @return array + * @return array */ protected function mergeHttpRequestArguments(ServerRequestInterface $httpRequest): array { @@ -50,9 +50,9 @@ protected function mergeHttpRequestArguments(ServerRequestInterface $httpRequest } /** - * @param array $arguments - * @param array $additionalArguments - * @return array + * @param array $arguments + * @param array $additionalArguments + * @return array */ protected function mergeHttpRequestArgumentsWithAdditionalArguments(array $arguments, array $additionalArguments): array { diff --git a/Neos.Flow/Classes/Mvc/ActionResponse.php b/Neos.Flow/Classes/Mvc/ActionResponse.php index 1600075a9f..29e80c6b51 100644 --- a/Neos.Flow/Classes/Mvc/ActionResponse.php +++ b/Neos.Flow/Classes/Mvc/ActionResponse.php @@ -80,7 +80,7 @@ final class ActionResponse protected $content; /** - * @var UriInterface + * @var ?UriInterface */ protected $redirectUri; @@ -95,7 +95,7 @@ final class ActionResponse protected $statusCode; /** - * @var string + * @var ?string */ protected $contentType; @@ -105,7 +105,7 @@ final class ActionResponse protected $cookies = []; /** - * @var array + * @var array */ protected $headers = []; @@ -204,7 +204,7 @@ public function deleteCookie(string $cookieName): void * 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 + * @param array|string|\DateTime $headerValue An array of values or a single value for the specified header field * @return void */ public function setHttpHeader(string $headerName, $headerValue): void @@ -224,7 +224,7 @@ public function setHttpHeader(string $headerName, $headerValue): void * 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 + * @param array|string|\DateTime $headerValue An array of values or a single value for the specified header field * @return void */ public function addHttpHeader(string $headerName, $headerValue): void @@ -241,7 +241,7 @@ public function addHttpHeader(string $headerName, $headerValue): void * Return the specified HTTP header that was previously set. * * @param string $headerName The name of the header to get the value(s) for - * @return array|string|null An array of field values if multiple headers of that name exist, a string value if only one value exists and NULL if there is no such header. + * @return array|string|null An array of field values if multiple headers of that name exist, a string value if only one value exists and NULL if there is no such header. */ public function getHttpHeader(string $headerName) { @@ -278,6 +278,9 @@ public function getStatusCode(): int return $this->statusCode ?? 200; } + /** + * @phpstan-assert-if-true non-empty-string $this->contentType + */ public function hasContentType(): bool { return !empty($this->contentType); diff --git a/Neos.Flow/Classes/Mvc/Controller/AbstractController.php b/Neos.Flow/Classes/Mvc/Controller/AbstractController.php index 41cf4e1db7..a7f1b4fdf8 100644 --- a/Neos.Flow/Classes/Mvc/Controller/AbstractController.php +++ b/Neos.Flow/Classes/Mvc/Controller/AbstractController.php @@ -57,7 +57,7 @@ abstract class AbstractController implements ControllerInterface /** * The current action request directed to this controller - * @var ActionRequest + * @var ?ActionRequest * @api */ protected $request; @@ -101,7 +101,7 @@ abstract class AbstractController implements ControllerInterface /** * A list of IANA media types which are supported by this controller * - * @var array + * @var array * @see http://www.iana.org/assignments/media-types/index.html */ protected $supportedMediaTypes = ['text/html']; @@ -121,7 +121,7 @@ abstract class AbstractController implements ControllerInterface * @param ActionRequest $request * @param ActionResponse $response */ - protected function initializeController(ActionRequest $request, ActionResponse $response) + protected function initializeController(ActionRequest $request, ActionResponse $response): void { // make the current request and response "globally" available to everywhere in this controller. $this->request = $request; @@ -163,18 +163,15 @@ public function getControllerContext() * @param string $messageBody text of the FlashMessage * @param string $messageTitle optional header of the FlashMessage * @param string $severity severity of the FlashMessage (one of the Message::SEVERITY_* constants) - * @param array $messageArguments arguments to be passed to the FlashMessage + * @param array $messageArguments arguments to be passed to the FlashMessage * @param integer $messageCode * @return void * @throws \InvalidArgumentException if the message body is no string * @see Error\Message * @api */ - public function addFlashMessage($messageBody, $messageTitle = '', $severity = Error\Message::SEVERITY_OK, array $messageArguments = [], $messageCode = null) + public function addFlashMessage(string $messageBody, $messageTitle = '', $severity = Error\Message::SEVERITY_OK, array $messageArguments = [], $messageCode = null) { - if (!is_string($messageBody)) { - throw new \InvalidArgumentException('The message body must be of type string, "' . gettype($messageBody) . '" given.', 1243258395); - } switch ($severity) { case Error\Message::SEVERITY_NOTICE: $message = new Error\Notice($messageBody, $messageCode, $messageArguments, $messageTitle); @@ -213,6 +210,9 @@ public function addFlashMessage($messageBody, $messageTitle = '', $severity = Er */ protected function forward(string $actionName, ?string $controllerName = null, ?string $packageKey = null, array $arguments = []): never { + if (!$this->request) { + throw new \Exception('Cannot resolve request', 1744328653); + } $nextRequest = clone $this->request; $nextRequest->setControllerActionName($actionName); @@ -290,7 +290,7 @@ protected function redirect(string $actionName, ?string $controllerName = null, } $this->uriBuilder->reset(); if ($format === null) { - $this->uriBuilder->setFormat($this->request->getFormat()); + $this->uriBuilder->setFormat($this->request?->getFormat()); } else { $this->uriBuilder->setFormat($format); } diff --git a/Neos.Flow/Classes/Mvc/Controller/ActionController.php b/Neos.Flow/Classes/Mvc/Controller/ActionController.php index 1c933828b4..7ce9ca38f2 100644 --- a/Neos.Flow/Classes/Mvc/Controller/ActionController.php +++ b/Neos.Flow/Classes/Mvc/Controller/ActionController.php @@ -37,6 +37,7 @@ use Neos\Flow\Reflection\ReflectionService; use Neos\Utility\TypeHandling; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; use Psr\Log\LoggerInterface; /** @@ -110,7 +111,7 @@ class ActionController extends AbstractController * * array('html' => 'MyCompany\MyApp\MyHtmlView', 'json' => 'MyCompany\... * - * @var array + * @var array> */ protected $viewFormatToObjectNameMap = []; @@ -145,7 +146,7 @@ class ActionController extends AbstractController protected $errorMethodName = 'errorAction'; /** - * @var array + * @var array */ protected $settings; @@ -168,7 +169,7 @@ class ActionController extends AbstractController protected $enableDynamicTypeValidation = false; /** - * @param array $settings + * @param array $settings * @return void */ public function injectSettings(array $settings) @@ -228,8 +229,9 @@ public function processRequest(ActionRequest $request): ResponseInterface $this->initializeAction(); $actionInitializationMethodName = 'initialize' . ucfirst($this->actionMethodName); - if (method_exists($this, $actionInitializationMethodName)) { - call_user_func([$this, $actionInitializationMethodName]); + $userFunc = [$this, $actionInitializationMethodName]; + if (is_callable($userFunc)) { + call_user_func($userFunc); } $this->mvcPropertyMappingConfigurationService->initializePropertyMappingConfigurationFromRequest($request, $this->arguments); @@ -330,7 +332,7 @@ protected function initializeActionMethodArguments(Arguments $arguments) * Returns a map of action method names and their parameters. * * @param ObjectManagerInterface $objectManager - * @return array Array of method parameters by action name + * @return array Array of method parameters by action name * @Flow\CompileStatic */ public static function getActionMethodParameters($objectManager) @@ -364,7 +366,7 @@ public static function getActionMethodParameters($objectManager) * This is a helper method purely used to make initializeActionMethodValidators() * testable without mocking static methods. * - * @return array + * @return array */ protected function getInformationNeededForInitializeActionMethodValidators() { @@ -436,7 +438,7 @@ protected function initializeActionMethodValidators(Arguments $arguments) * Returns a map of action method names and their validation groups. * * @param ObjectManagerInterface $objectManager - * @return array Array of validation groups by action method name + * @return array> Array of validation groups by action method name * @Flow\CompileStatic */ public static function getActionValidationGroups($objectManager) @@ -463,7 +465,7 @@ public static function getActionValidationGroups($objectManager) * Returns a map of action method names and their validation parameters. * * @param ObjectManagerInterface $objectManager - * @return array Array of validate annotation parameters by action method name + * @return array, argumentName: string}>> Array of validate annotation parameters by action method name * @Flow\CompileStatic */ public static function getActionValidateAnnotationData($objectManager) @@ -562,6 +564,7 @@ protected function callActionMethod(ActionRequest $request, Arguments $arguments return $actionResult; } + /** @phpstan-ignore instanceof.alwaysTrue (fixme: weird initial null state) */ if ($actionResult === null && $this->view instanceof ViewInterface) { $result = $this->view->render(); @@ -571,6 +574,10 @@ protected function callActionMethod(ActionRequest $request, Arguments $arguments return $result; } + if (!$result instanceof StreamInterface) { + throw new \Exception('Cannot assign object of type ' . get_class($result) . ' to HTTP response body', 1744326907); + } + return $httpResponse->withBody($result); } @@ -579,7 +586,7 @@ protected function callActionMethod(ActionRequest $request, Arguments $arguments /** * @param ObjectManagerInterface $objectManager - * @return array Array of argument names as key by action method name + * @return array> Array of argument names as key by action method name * @Flow\CompileStatic */ public static function getActionIgnoredValidationArguments($objectManager) @@ -610,7 +617,7 @@ public static function getActionIgnoredValidationArguments($objectManager) /** * @param ObjectManagerInterface $objectManager - * @return array Array of all public action method names, indexed by method name + * @return array Array of all public action method names, indexed by method name * @Flow\CompileStatic */ public static function getPublicActionMethods($objectManager) @@ -678,7 +685,7 @@ protected function resolveView(ActionRequest $request) * @param ViewInterface $view * @Flow\Signal */ - protected function emitViewResolved(ViewInterface $view) + protected function emitViewResolved(ViewInterface $view): void { } @@ -755,10 +762,11 @@ protected function errorAction() */ protected function handleTargetNotFoundError() { - foreach (array_keys($this->request->getArguments()) as $argumentName) { - /** @var TargetNotFoundError $targetNotFoundError */ - $targetNotFoundError = $this->arguments->getValidationResults()->forProperty($argumentName)->getFirstError(TargetNotFoundError::class); - if ($targetNotFoundError !== false) { + foreach (array_keys($this->request?->getArguments() ?: []) as $argumentName) { + $targetNotFoundError = $this->arguments->getValidationResults() + ->forProperty($argumentName) + ->getFirstError(TargetNotFoundError::class); + if ($targetNotFoundError instanceof TargetNotFoundError) { throw new TargetNotFoundException($targetNotFoundError->getMessage(), $targetNotFoundError->getCode()); } } @@ -788,7 +796,7 @@ protected function addErrorFlashMessage() */ protected function forwardToReferringRequest() { - $referringRequest = $this->request->getReferringRequest(); + $referringRequest = $this->request?->getReferringRequest(); if ($referringRequest === null) { return; } @@ -798,7 +806,7 @@ protected function forwardToReferringRequest() $packageKey .= '\\' . $subpackageKey; } $argumentsForNextController = $referringRequest->getArguments(); - $argumentsForNextController['__submittedArguments'] = $this->request->getArguments(); + $argumentsForNextController['__submittedArguments'] = $this->request?->getArguments() ?: []; $argumentsForNextController['__submittedArgumentValidationResults'] = $this->arguments->getValidationResults(); $this->forward($referringRequest->getControllerActionName(), $referringRequest->getControllerName(), $packageKey, $argumentsForNextController); diff --git a/Neos.Flow/Classes/Mvc/Controller/Argument.php b/Neos.Flow/Classes/Mvc/Controller/Argument.php index c125a2f564..dfdce6a42c 100644 --- a/Neos.Flow/Classes/Mvc/Controller/Argument.php +++ b/Neos.Flow/Classes/Mvc/Controller/Argument.php @@ -182,7 +182,7 @@ public function setValidator(ValidatorInterface $validator): Argument { $this->validator = $validator; // the validation should not be called on null values - for the cases where the value is required, the error will be thrown in Controller::mapRequestArgumentsToControllerArguments() - if ($validator !== null && $this->value !== null) { + if ($this->value !== null) { $this->validationResults = $this->propertyMapper->getMessages() ?? new Result(); $validationMessages = $validator->validate($this->value); $this->validationResults->merge($validationMessages); @@ -227,7 +227,7 @@ public function setValue($rawValue): Argument $this->dataType = $rawValue['__type']; } $this->value = $this->propertyMapper->convert($rawValue, $this->dataType, $this->getPropertyMappingConfiguration()); - $this->validationResults = $this->propertyMapper->getMessages() ?? new Result(); + $this->validationResults = $this->propertyMapper->getMessages(); if ($this->validator !== null) { $validationMessages = $this->validator->validate($this->value); $this->validationResults->merge($validationMessages); diff --git a/Neos.Flow/Classes/Mvc/Controller/Arguments.php b/Neos.Flow/Classes/Mvc/Controller/Arguments.php index bae2153ffe..3e4a5d6c2c 100644 --- a/Neos.Flow/Classes/Mvc/Controller/Arguments.php +++ b/Neos.Flow/Classes/Mvc/Controller/Arguments.php @@ -24,7 +24,7 @@ class Arguments extends \ArrayObject { /** * Names of the arguments contained by this object - * @var array + * @var array */ protected array $argumentNames = []; @@ -75,6 +75,9 @@ public function append($value): void public function offsetUnset($offset): void { $translatedOffset = $this->validateArgumentExistence($offset); + if ($translatedOffset === false) { + throw new \Exception('Missing argument', 1744326231); + } parent::offsetUnset($translatedOffset); unset($this->argumentNames[$translatedOffset]); @@ -90,6 +93,9 @@ public function offsetUnset($offset): void public function offsetExists($offset): bool { $translatedOffset = $this->validateArgumentExistence($offset); + if ($translatedOffset === false) { + throw new \Exception('Missing argument', 1744326202); + } return parent::offsetExists($translatedOffset); } @@ -97,7 +103,7 @@ public function offsetExists($offset): bool * Returns the value at the specified index * * @param mixed $offset Offset - * @return Argument The requested argument object + * @return ?Argument The requested argument object * @throws NoSuchArgumentException if the argument does not exist * @api */ @@ -154,7 +160,7 @@ public function addArgument(Argument $argument) * Returns an argument specified by name * * @param string $argumentName Name of the argument to retrieve - * @return Argument + * @return ?Argument * @throws NoSuchArgumentException * @api */ @@ -179,7 +185,7 @@ public function hasArgument($argumentName) /** * Returns the names of all arguments contained in this object * - * @return array Argument names + * @return array Argument names * @api */ public function getArgumentNames() @@ -192,7 +198,7 @@ public function getArgumentNames() * value can be set by just calling the setArgumentName() method. * * @param string $methodName Name of the method - * @param array $arguments Method arguments + * @param array $arguments Method arguments * @return void * @throws \LogicException */ @@ -202,14 +208,20 @@ public function __call($methodName, array $arguments) throw new \LogicException('Unknown method "' . $methodName . '".', 1210858451); } $firstLowerCaseArgumentName = $this->validateArgumentExistence(strtolower($methodName[3]) . substr($methodName, 4)); + if ($firstLowerCaseArgumentName === false) { + throw new \Exception('Missing argument', 1744326132); + } $firstUpperCaseArgumentName = $this->validateArgumentExistence(ucfirst(substr($methodName, 3))); + if ($firstUpperCaseArgumentName === false) { + throw new \Exception('Missing argument', 1744326142); + } if (in_array($firstLowerCaseArgumentName, $this->getArgumentNames())) { $argument = parent::offsetGet($firstLowerCaseArgumentName); - $argument->setValue($arguments[0]); + $argument?->setValue($arguments[0]); } elseif (in_array($firstUpperCaseArgumentName, $this->getArgumentNames())) { $argument = parent::offsetGet($firstUpperCaseArgumentName); - $argument->setValue($arguments[0]); + $argument?->setValue($arguments[0]); } } diff --git a/Neos.Flow/Classes/Mvc/Controller/MvcPropertyMappingConfigurationService.php b/Neos.Flow/Classes/Mvc/Controller/MvcPropertyMappingConfigurationService.php index 2ad52daefe..c9ede36b8d 100644 --- a/Neos.Flow/Classes/Mvc/Controller/MvcPropertyMappingConfigurationService.php +++ b/Neos.Flow/Classes/Mvc/Controller/MvcPropertyMappingConfigurationService.php @@ -48,7 +48,7 @@ class MvcPropertyMappingConfigurationService /** * Generate a request hash for a list of form fields * - * @param array $formFieldNames Array of form fields + * @param array $formFieldNames Array of form fields * @param string $fieldNamePrefix * @return string trusted properties token * @throws InvalidArgumentForHashGenerationException @@ -69,6 +69,7 @@ public function generateTrustedPropertiesToken($formFieldNames, $fieldNamePrefix } if ($i === count($formFieldParts) - 1) { + /** @phpstan-ignore booleanAnd.rightAlwaysFalse (not sure, though) */ if (isset($currentPosition[$formFieldPart]) && is_array($currentPosition[$formFieldPart])) { throw new InvalidArgumentForHashGenerationException('The form field "' . $formField . '" is declared as string, but it collides with a previous form field of the same name which declared the field as array. This is an inconsistency you need to fix inside your generated form. (Array overridden by String)', 1255072587); } @@ -90,6 +91,7 @@ public function generateTrustedPropertiesToken($formFieldNames, $fieldNamePrefix } } if ($fieldNamePrefix !== '') { + /** @phpstan-ignore nullCoalesce.offset (set by reference) */ $formFieldArray = $formFieldArray[$fieldNamePrefix] ?? []; } return $this->serializeAndHashFormFieldArray($formFieldArray); @@ -98,7 +100,7 @@ public function generateTrustedPropertiesToken($formFieldNames, $fieldNamePrefix /** * Serialize and hash the form field array * - * @param array $formFieldArray form field array to be serialized and hashed + * @param array $formFieldArray form field array to be serialized and hashed * @return string Hash */ protected function serializeAndHashFormFieldArray($formFieldArray) @@ -128,10 +130,11 @@ public function initializePropertyMappingConfigurationFromRequest(ActionRequest $trustedProperties = unserialize($serializedTrustedProperties); foreach ($trustedProperties as $propertyName => $propertyConfiguration) { - if (!$controllerArguments->hasArgument($propertyName)) { + $argument = $controllerArguments->getArgument($propertyName); + if (!$argument) { continue; } - $propertyMappingConfiguration = $controllerArguments->getArgument($propertyName)->getPropertyMappingConfiguration(); + $propertyMappingConfiguration = $argument->getPropertyMappingConfiguration(); $this->modifyPropertyMappingConfiguration($propertyConfiguration, $propertyMappingConfiguration); } } @@ -143,7 +146,7 @@ public function initializePropertyMappingConfigurationFromRequest(ActionRequest * * All other properties are specified as allowed properties. * - * @param array $propertyConfiguration + * @param mixed $propertyConfiguration * @param PropertyMappingConfiguration $propertyMappingConfiguration * @return void */ diff --git a/Neos.Flow/Classes/Mvc/Controller/RestController.php b/Neos.Flow/Classes/Mvc/Controller/RestController.php index 849c637e94..122fd5cc1e 100644 --- a/Neos.Flow/Classes/Mvc/Controller/RestController.php +++ b/Neos.Flow/Classes/Mvc/Controller/RestController.php @@ -99,7 +99,10 @@ protected function resolveActionMethodName(ActionRequest $request): string */ protected function initializeCreateAction() { - $propertyMappingConfiguration = $this->arguments[$this->resourceArgumentName]->getPropertyMappingConfiguration(); + $propertyMappingConfiguration = $this->arguments[$this->resourceArgumentName]?->getPropertyMappingConfiguration(); + if (!$propertyMappingConfiguration) { + throw new \Exception('Missing property mapping configuration for ' . $this->resourceArgumentName, 1744328226); + } $propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_CREATION_ALLOWED, true); $propertyMappingConfiguration->allowAllProperties(); } @@ -111,7 +114,10 @@ protected function initializeCreateAction() */ protected function initializeUpdateAction() { - $propertyMappingConfiguration = $this->arguments[$this->resourceArgumentName]->getPropertyMappingConfiguration(); + $propertyMappingConfiguration = $this->arguments[$this->resourceArgumentName]?->getPropertyMappingConfiguration(); + if (!$propertyMappingConfiguration) { + throw new \Exception('Missing property mapping configuration for ' . $this->resourceArgumentName, 1744328226); + } $propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_MODIFICATION_ALLOWED, true); $propertyMappingConfiguration->allowAllProperties(); } diff --git a/Neos.Flow/Classes/Mvc/Dispatcher.php b/Neos.Flow/Classes/Mvc/Dispatcher.php index 8d47161edf..715884cdca 100644 --- a/Neos.Flow/Classes/Mvc/Dispatcher.php +++ b/Neos.Flow/Classes/Mvc/Dispatcher.php @@ -68,7 +68,7 @@ public function injectObjectManager(ObjectManagerInterface $objectManager) /** * @param Context $context */ - public function injectSecurityContext(Context $context) + public function injectSecurityContext(Context $context): void { $this->securityContext = $context; } @@ -76,7 +76,7 @@ public function injectSecurityContext(Context $context) /** * @param FirewallInterface $firewall */ - public function injectFirewall(FirewallInterface $firewall) + public function injectFirewall(FirewallInterface $firewall): void { $this->firewall = $firewall; } @@ -193,7 +193,7 @@ protected function resolveController(ActionRequest $request): ControllerInterfac $controller = $this->objectManager->get($controllerObjectName); if (!$controller instanceof ControllerInterface) { - throw new Controller\Exception\InvalidControllerException('Invalid controller "' . $request->getControllerObjectName() . '". The controller must be a valid request handling controller, ' . (is_object($controller) ? get_class($controller) : gettype($controller)) . ' given.', 1202921619, null, $request); + throw new Controller\Exception\InvalidControllerException('Invalid controller "' . $request->getControllerObjectName() . '". The controller must be a valid request handling controller, ' . get_class($controller) . ' given.', 1202921619, null, $request); } return $controller; } diff --git a/Neos.Flow/Classes/Mvc/FlashMessage/FlashMessageContainer.php b/Neos.Flow/Classes/Mvc/FlashMessage/FlashMessageContainer.php index c8f274eedd..12ecadc80e 100644 --- a/Neos.Flow/Classes/Mvc/FlashMessage/FlashMessageContainer.php +++ b/Neos.Flow/Classes/Mvc/FlashMessage/FlashMessageContainer.php @@ -22,7 +22,7 @@ class FlashMessageContainer { /** - * @var array + * @var array */ protected $messages = []; diff --git a/Neos.Flow/Classes/Mvc/FlashMessage/FlashMessageService.php b/Neos.Flow/Classes/Mvc/FlashMessage/FlashMessageService.php index 48dad0da21..f7f0bb02e0 100644 --- a/Neos.Flow/Classes/Mvc/FlashMessage/FlashMessageService.php +++ b/Neos.Flow/Classes/Mvc/FlashMessage/FlashMessageService.php @@ -41,7 +41,7 @@ class FlashMessageService /** * @Flow\InjectConfiguration(path="mvc.flashMessages.containers") - * @var array + * @var array */ protected $flashMessageContainerConfiguration; diff --git a/Neos.Flow/Classes/Mvc/FlashMessage/Storage/FlashMessageCookieStorage.php b/Neos.Flow/Classes/Mvc/FlashMessage/Storage/FlashMessageCookieStorage.php index ee1da13493..b5b4f292f0 100644 --- a/Neos.Flow/Classes/Mvc/FlashMessage/Storage/FlashMessageCookieStorage.php +++ b/Neos.Flow/Classes/Mvc/FlashMessage/Storage/FlashMessageCookieStorage.php @@ -26,7 +26,7 @@ class FlashMessageCookieStorage implements FlashMessageStorageInterface const DEFAULT_COOKIE_NAME = 'Neos_Flow_FlashMessages'; /** - * @var array + * @var array */ private $options; @@ -41,7 +41,7 @@ class FlashMessageCookieStorage implements FlashMessageStorageInterface private $flashMessageContainer; /** - * @param array $options + * @param array $options */ public function __construct(array $options = []) { @@ -100,13 +100,13 @@ public function persist(HttpResponseInterface $response): HttpResponseInterface foreach ($this->flashMessageContainer->getMessagesAndFlush() as $flashMessage) { $serializedMessages[] = $this->serializeMessage($flashMessage); } - $cookie = new Cookie($this->cookieName, json_encode($serializedMessages), 0, null, null, '/', false, false); + $cookie = new Cookie($this->cookieName, json_encode($serializedMessages, JSON_THROW_ON_ERROR), 0, null, null, '/', false, false); return $response->withAddedHeader('Set-Cookie', (string)$cookie); } /** * @param Message $message - * @return array + * @return array */ private function serializeMessage(Message $message): array { @@ -121,7 +121,7 @@ private function serializeMessage(Message $message): array } /** - * @param array $messageArray + * @param array $messageArray * @return Message */ private function deserializeMessage(array $messageArray): Message diff --git a/Neos.Flow/Classes/Mvc/FlashMessage/Storage/FlashMessageSessionStorage.php b/Neos.Flow/Classes/Mvc/FlashMessage/Storage/FlashMessageSessionStorage.php index e0ecc97a02..44cbf721da 100644 --- a/Neos.Flow/Classes/Mvc/FlashMessage/Storage/FlashMessageSessionStorage.php +++ b/Neos.Flow/Classes/Mvc/FlashMessage/Storage/FlashMessageSessionStorage.php @@ -29,7 +29,7 @@ class FlashMessageSessionStorage implements FlashMessageStorageInterface protected $session; /** - * @var array + * @var array */ private $options; @@ -44,7 +44,7 @@ class FlashMessageSessionStorage implements FlashMessageStorageInterface private $flashMessageContainer; /** - * @param array $options + * @param array $options */ public function __construct(array $options = []) { diff --git a/Neos.Flow/Classes/Mvc/RequestMatcher.php b/Neos.Flow/Classes/Mvc/RequestMatcher.php index a82b8e5050..4ca759c9bd 100644 --- a/Neos.Flow/Classes/Mvc/RequestMatcher.php +++ b/Neos.Flow/Classes/Mvc/RequestMatcher.php @@ -30,7 +30,7 @@ class RequestMatcher * return false. This case is primarily needed * if no parentRequest exists. * - * @var ActionRequest + * @var ?ActionRequest */ protected $request; @@ -41,7 +41,7 @@ class RequestMatcher * and mainRequest Matchers through the addWeight * method * - * @var RequestMatcher + * @var ?RequestMatcher */ protected $parentMatcher; @@ -163,7 +163,7 @@ public function getParentRequest() return new RequestMatcher(); } $this->addWeight(1000000); - return new RequestMatcher($this->request->getParentRequest(), $this); + return new RequestMatcher($this->request?->getParentRequest(), $this); } /** @@ -175,7 +175,7 @@ public function getParentRequest() public function getMainRequest() { $this->addWeight(100000); - return new RequestMatcher($this->request->getMainRequest(), $this); + return new RequestMatcher($this->request?->getMainRequest(), $this); } /** diff --git a/Neos.Flow/Classes/Mvc/Routing/AbstractRoutePart.php b/Neos.Flow/Classes/Mvc/Routing/AbstractRoutePart.php index 18e7b8c60f..9b49add5ff 100644 --- a/Neos.Flow/Classes/Mvc/Routing/AbstractRoutePart.php +++ b/Neos.Flow/Classes/Mvc/Routing/AbstractRoutePart.php @@ -20,7 +20,7 @@ abstract class AbstractRoutePart implements RoutePartInterface /** * Name of the Route Part * - * @var string + * @var ?string */ protected $name = null; @@ -55,7 +55,7 @@ abstract class AbstractRoutePart implements RoutePartInterface /** * Contains options for this Route Part. * - * @var array + * @var array */ protected $options = []; @@ -73,7 +73,7 @@ public function setName($partName) /** * Returns name of the Route Part. * - * @return string + * @return ?string */ public function getName() { @@ -180,7 +180,7 @@ public function isLowerCase() * Defines options for this Route Part. * Options can be used to enrich a route part with parameters or settings like case sensivity. * - * @param array $options + * @param array $options * @return void */ public function setOptions(array $options) @@ -189,7 +189,7 @@ public function setOptions(array $options) } /** - * @return array options of this Route Part. + * @return array options of this Route Part. */ public function getOptions() { diff --git a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php index 039215da9c..b62128a9ee 100644 --- a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php +++ b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php @@ -92,13 +92,25 @@ public function getRoutes(): Routes $routes = [...$routes, ...$routesForClass]; } - $routes = array_map(static fn (array $routeConfiguration): Route => Route::fromConfiguration($routeConfiguration), $routes); + $routes = array_map( + static fn (array $routeConfiguration): Route => Route::fromConfiguration($routeConfiguration), + $routes + ); return Routes::create(...$routes); } /** * @param ObjectManagerInterface $objectManager - * @return array> + * @return array, + * routeParts?: array, + * toLowerCase?: bool, + * appendExceedingArguments?: bool, + * cache?: array{lifetime?: int, tags?: array}, + * httpMethods?: list + * }>> * @throws InvalidActionNameException * @throws InvalidControllerException * @throws \Neos\Flow\Utility\Exception @@ -119,7 +131,10 @@ public static function compileRoutesConfiguration(ObjectManagerInterface $object } $controllerObjectName = $objectManager->getCaseSensitiveObjectName($className); - $controllerPackageKey = $objectManager->getPackageKeyByObjectName($controllerObjectName); + $controllerPackageKey = $controllerObjectName ? $objectManager->getPackageKeyByObjectName($controllerObjectName) : null; + if (!$controllerPackageKey) { + throw new \Exception('Could not resolve package key for class ' . $className); + } $controllerPackageNamespace = str_replace('.', '\\', $controllerPackageKey); if (!str_ends_with($className, 'Controller')) { throw new InvalidControllerException('Only for controller classes'); @@ -144,26 +159,24 @@ public static function compileRoutesConfiguration(ObjectManagerInterface $object } $annotations = $reflectionService->getMethodAnnotations($className, $methodName, Flow\Route::class); foreach ($annotations as $annotation) { - if ($annotation instanceof Flow\Route) { - $controller = substr($controllerName, 0, -10); - $action = substr($methodName, 0, -6); - $configuration = [ - 'name' => $controllerPackageKey . ' :: ' . $controller . ' :: ' . ($annotation->name ?: $action), - 'uriPattern' => $annotation->uriPattern, - 'httpMethods' => $annotation->httpMethods, - 'defaults' => Arrays::arrayMergeRecursiveOverrule( - [ - '@package' => $controllerPackageKey, - '@subpackage' => $subPackage, - '@controller' => $controller, - '@action' => $action, - '@format' => 'html' - ], - $annotation->defaults ?? [] - ) - ]; - $routesByClassName[$className][] = $configuration; - } + $controller = substr($controllerName, 0, -10); + $action = substr($methodName, 0, -6); + $configuration = [ + 'name' => $controllerPackageKey . ' :: ' . $controller . ' :: ' . ($annotation->name ?: $action), + 'uriPattern' => $annotation->uriPattern, + 'httpMethods' => $annotation->httpMethods, + 'defaults' => Arrays::arrayMergeRecursiveOverrule( + [ + '@package' => $controllerPackageKey, + '@subpackage' => $subPackage, + '@controller' => $controller, + '@action' => $action, + '@format' => 'html' + ], + $annotation->defaults ?? [] + ) + ]; + $routesByClassName[$className][] = $configuration; } } } diff --git a/Neos.Flow/Classes/Mvc/Routing/Dto/ResolveContext.php b/Neos.Flow/Classes/Mvc/Routing/Dto/ResolveContext.php index 64e8d11318..d488159562 100644 --- a/Neos.Flow/Classes/Mvc/Routing/Dto/ResolveContext.php +++ b/Neos.Flow/Classes/Mvc/Routing/Dto/ResolveContext.php @@ -31,7 +31,7 @@ final class ResolveContext /** * Route values to build the URI, for example ['@action' => 'index', 'someArgument' => 'foo', ...] * - * @var array + * @var array */ private $routeValues; @@ -56,7 +56,7 @@ final class ResolveContext /** * @param UriInterface $baseUri The base URI, retrieved from the current request URI or from configuration, if specified. Required to fill in parts of the result when resolving absolute URIs - * @param array $routeValues Route values to build the URI, for example ['@action' => 'index', 'someArgument' => 'foo', ...] + * @param array $routeValues Route values to build the URI, for example ['@action' => 'index', 'someArgument' => 'foo', ...] * @param bool $forceAbsoluteUri Whether or not an absolute URI is to be returned * @param string $uriPathPrefix A prefix to be prepended to any resolved URI. Not allowed to start with "/". * @param RouteParameters $parameters @@ -83,7 +83,7 @@ public function getBaseUri(): UriInterface } /** - * @return array + * @return array */ public function getRouteValues(): array { diff --git a/Neos.Flow/Classes/Mvc/Routing/Dto/RouteParameters.php b/Neos.Flow/Classes/Mvc/Routing/Dto/RouteParameters.php index aa1dab51f2..b17eac637f 100644 --- a/Neos.Flow/Classes/Mvc/Routing/Dto/RouteParameters.php +++ b/Neos.Flow/Classes/Mvc/Routing/Dto/RouteParameters.php @@ -29,12 +29,12 @@ final class RouteParameters implements CacheAwareInterface /** * The parameters as simple key/value pair in the format ['' => , ...] * - * @var array + * @var array */ private $parameters; /** - * @param array $parameters simple key/value pair in the format ['' => , ...] + * @param array $parameters simple key/value pair in the format ['' => , ...] */ private function __construct(array $parameters) { @@ -61,7 +61,7 @@ public static function createEmpty(): self public function withParameter(string $parameterName, $parameterValue): self { if (!TypeHandling::isLiteral(gettype($parameterValue)) && (!$parameterValue instanceof CacheAwareInterface)) { - throw new \InvalidArgumentException(sprintf('Parameter values must be literal types or implement the CacheAwareInterface, given: "%s"', is_object($parameterValue) ? get_class($parameterValue) : gettype($parameterValue)), 1511194273); + throw new \InvalidArgumentException(sprintf('Parameter values must be literal types or implement the CacheAwareInterface, given: "%s"', gettype($parameterValue)), 1511194273); } $newParameters = $this->parameters; $newParameters[$parameterName] = $parameterValue; @@ -99,7 +99,7 @@ public function isEmpty(): bool } /** - * @return array + * @return array */ public function toArray(): array { diff --git a/Neos.Flow/Classes/Mvc/Routing/Dto/RouteTags.php b/Neos.Flow/Classes/Mvc/Routing/Dto/RouteTags.php index 44a1833079..865ca12583 100644 --- a/Neos.Flow/Classes/Mvc/Routing/Dto/RouteTags.php +++ b/Neos.Flow/Classes/Mvc/Routing/Dto/RouteTags.php @@ -29,7 +29,7 @@ final class RouteTags const PATTERN_TAG = '/^[a-zA-Z0-9_%\-&]{1,250}$/'; /** - * @var string[] numeric array of strings satisfying the PATTERN_TAG regex + * @var array numeric array of strings satisfying the PATTERN_TAG regex */ private $tags = []; @@ -113,11 +113,8 @@ public function withTag(string $tag): self * @param string $tag * @throws \InvalidArgumentException */ - private static function validateTag($tag) + private static function validateTag(string $tag): void { - if (!is_string($tag)) { - throw new \InvalidArgumentException(sprintf('RouteTags have to be strings, %s given', is_object($tag) ? get_class($tag) : gettype($tag)), 1512553153); - } if (preg_match(self::PATTERN_TAG, $tag) !== 1) { throw new \InvalidArgumentException(sprintf('The given string "%s" is not a valid tag', $tag), 1511807639); } @@ -137,7 +134,7 @@ public function has(string $tag) /** * Returns the tags of this tag collection as value array * - * @return array + * @return array */ public function getTags(): array { diff --git a/Neos.Flow/Classes/Mvc/Routing/Dto/UriConstraints.php b/Neos.Flow/Classes/Mvc/Routing/Dto/UriConstraints.php index cfead76153..1b2a235e1b 100644 --- a/Neos.Flow/Classes/Mvc/Routing/Dto/UriConstraints.php +++ b/Neos.Flow/Classes/Mvc/Routing/Dto/UriConstraints.php @@ -48,12 +48,12 @@ final class UriConstraints const HTTP_DEFAULT_HOST = 'localhost'; /** - * @var array + * @var array */ private $constraints; /** - * @param array $constraints array of constraints with one of the CONSTRAINT_* constants as keys and corresponding values + * @param array $constraints array of constraints with one of the CONSTRAINT_* constants as keys and corresponding values */ private function __construct(array $constraints) { @@ -212,7 +212,7 @@ public function withQueryString(string $queryString): self /** * Create a new instance with a query string corresponding to the given $values merged with any existing query string constraint * - * @param array $values + * @param array $values * @return UriConstraints */ public function withAddedQueryValues(array $values): self diff --git a/Neos.Flow/Classes/Mvc/Routing/DynamicRoutePart.php b/Neos.Flow/Classes/Mvc/Routing/DynamicRoutePart.php index b7c9e62347..29b1c6a02a 100644 --- a/Neos.Flow/Classes/Mvc/Routing/DynamicRoutePart.php +++ b/Neos.Flow/Classes/Mvc/Routing/DynamicRoutePart.php @@ -140,7 +140,7 @@ protected function findValueToMatch($routePath) */ protected function matchValue($value) { - if ($value === null || $value === '') { + if ($value === '') { return false; } return new MatchResult(rawurldecode($value)); @@ -157,7 +157,7 @@ protected function matchValue($value) */ protected function removeMatchingPortionFromRequestPath(&$routePath, $valueToMatch) { - if ($valueToMatch !== null && $valueToMatch !== '') { + if ($valueToMatch !== '') { $routePath = substr($routePath, strlen($valueToMatch)); } } @@ -167,7 +167,7 @@ protected function removeMatchingPortionFromRequestPath(&$routePath, $valueToMat * If a corresponding element is found in $routeValues, this element is removed from the array. * @see resolveWithParameters() * - * @param array $routeValues An array with key/value pairs to be resolved by Dynamic Route Parts. + * @param array $routeValues An array with key/value pairs to be resolved by Dynamic Route Parts. * @return bool|ResolveResult true or an instance of ResolveResult if current Route Part could be resolved, otherwise false */ final public function resolve(array &$routeValues) @@ -179,7 +179,7 @@ final public function resolve(array &$routeValues) * Checks whether $routeValues contains elements which correspond to this Dynamic Route Part. * If a corresponding element is found in $routeValues, this element is removed from the array. * - * @param array $routeValues + * @param array $routeValues * @param RouteParameters $parameters * @return bool|ResolveResult */ @@ -203,8 +203,8 @@ final public function resolveWithParameters(array &$routeValues, RouteParameters * Returns the route value of the current route part. * This method can be overridden by custom RoutePartHandlers to implement custom resolving mechanisms. * - * @param array $routeValues An array with key/value pairs to be resolved by Dynamic Route Parts. - * @return string|array|null value to resolve. + * @param array $routeValues An array with key/value pairs to be resolved by Dynamic Route Parts. + * @return string|array|null value to resolve. * @api */ protected function findValueToResolve(array $routeValues) diff --git a/Neos.Flow/Classes/Mvc/Routing/IdentityRoutePart.php b/Neos.Flow/Classes/Mvc/Routing/IdentityRoutePart.php index f62a9afa79..45899b32b2 100644 --- a/Neos.Flow/Classes/Mvc/Routing/IdentityRoutePart.php +++ b/Neos.Flow/Classes/Mvc/Routing/IdentityRoutePart.php @@ -124,7 +124,7 @@ public function getUriPattern() */ protected function matchValue($value) { - if ($value === null || $value === '') { + if ($value === '') { return false; } $identifier = $this->getObjectIdentifierFromPathSegment($value); @@ -202,13 +202,10 @@ protected function resolveValue($value) } elseif ($value instanceof $this->objectType) { $identifier = $this->persistenceManager->getIdentifierByObject($value); } - if ($identifier === null || (!is_string($identifier) && !is_integer($identifier))) { + if (!is_string($identifier)) { return false; } $pathSegment = $this->getPathSegmentByIdentifier($identifier); - if ($pathSegment === null) { - return false; - } $this->value = $pathSegment; return true; } @@ -277,7 +274,7 @@ protected function createPathSegmentForObject($object) $dateFormat = isset($dynamicPathSegmentParts[1]) ? trim($dynamicPathSegmentParts[1]) : 'Y-m-d'; $pathSegment .= $this->rewriteForUri($dynamicPathSegment->format($dateFormat)); } else { - throw new InvalidUriPatternException(sprintf('Invalid uriPattern "%s" for route part "%s". Property "%s" must be of type string or \DateTime. "%s" given.', $this->getUriPattern(), $this->getName(), $propertyPath, is_object($dynamicPathSegment) ? get_class($dynamicPathSegment) : gettype($dynamicPathSegment)), 1316442409); + throw new InvalidUriPatternException(sprintf('Invalid uriPattern "%s" for route part "%s". Property "%s" must be of type string or \DateTime. "%s" given.', $this->getUriPattern(), $this->getName(), $propertyPath, get_class($dynamicPathSegment)), 1316442409); } } else { $pathSegment .= $this->rewriteForUri((string)$dynamicPathSegment); @@ -291,7 +288,7 @@ protected function createPathSegmentForObject($object) * Creates a new ObjectPathMapping and stores it in the repository * * @param string $pathSegment - * @param string|integer $identifier + * @param string $identifier * @return void */ protected function storeObjectPathMapping($pathSegment, $identifier) @@ -327,12 +324,12 @@ protected function rewriteForUri(string $value) $value = strtr($value, $transliteration); $spaceCharacter = '-'; - $value = preg_replace('/[ \-+_]+/', $spaceCharacter, $value); + $value = preg_replace('/[ \-+_]+/', $spaceCharacter, $value) ?: ''; - $value = preg_replace('/[^-a-z0-9.\\' . $spaceCharacter . ']/i', '', $value); + $value = preg_replace('/[^-a-z0-9.\\' . $spaceCharacter . ']/i', '', $value) ?: ''; - $value = preg_replace('/\\' . $spaceCharacter . '{2,}/', $spaceCharacter, $value); - $value = trim($value, $spaceCharacter); + $value = preg_replace('/\\' . $spaceCharacter . '{2,}/', $spaceCharacter, $value) ?: ''; + $value = trim($value, $spaceCharacter) ?: ''; return $value; } diff --git a/Neos.Flow/Classes/Mvc/Routing/ObjectPathMappingRepository.php b/Neos.Flow/Classes/Mvc/Routing/ObjectPathMappingRepository.php index e6eabd2b5a..2468ad8c99 100644 --- a/Neos.Flow/Classes/Mvc/Routing/ObjectPathMappingRepository.php +++ b/Neos.Flow/Classes/Mvc/Routing/ObjectPathMappingRepository.php @@ -38,7 +38,7 @@ class ObjectPathMappingRepository extends Repository protected $entityManager; /** - * @var array + * @var array */ protected $defaultOrderings = [ 'objectType' => QueryInterface::ORDER_ASCENDING, @@ -55,7 +55,7 @@ class ObjectPathMappingRepository extends Repository public function findOneByObjectTypeUriPatternAndPathSegment($objectType, $uriPattern, $pathSegment, $caseSensitive = false) { $query = $this->createQuery(); - return $query->matching( + $result = $query->matching( $query->logicalAnd( $query->equals('objectType', $objectType), $query->equals('uriPattern', $uriPattern), @@ -64,6 +64,8 @@ public function findOneByObjectTypeUriPatternAndPathSegment($objectType, $uriPat ) ->execute() ->getFirst(); + + return $result instanceof ObjectPathMapping ? $result : null; } /** @@ -76,7 +78,7 @@ public function findOneByObjectTypeUriPatternAndPathSegment($objectType, $uriPat public function findOneByObjectTypeUriPatternAndIdentifier($objectType, $uriPattern, $identifier) { $query = $this->createQuery(); - return $query->matching( + $result = $query->matching( $query->logicalAnd( $query->equals('objectType', $objectType), $query->equals('uriPattern', $uriPattern), @@ -85,6 +87,8 @@ public function findOneByObjectTypeUriPatternAndIdentifier($objectType, $uriPatt ) ->execute() ->getFirst(); + + return $result instanceof ObjectPathMapping ? $result : null; } /** diff --git a/Neos.Flow/Classes/Mvc/Routing/ParameterAwareRoutePartInterface.php b/Neos.Flow/Classes/Mvc/Routing/ParameterAwareRoutePartInterface.php index 6414a20e5a..ce2774dc47 100644 --- a/Neos.Flow/Classes/Mvc/Routing/ParameterAwareRoutePartInterface.php +++ b/Neos.Flow/Classes/Mvc/Routing/ParameterAwareRoutePartInterface.php @@ -37,7 +37,7 @@ public function matchWithParameters(&$routePath, RouteParameters $parameters); * removes resolved elements from $routeValues-Array. * This is why $routeValues has to be passed by reference. * - * @param array &$routeValues An array with key/value pairs to be resolved by Dynamic Route Parts. + * @param array &$routeValues An array with key/value pairs to be resolved by Dynamic Route Parts. * @param RouteParameters $parameters The Routing RouteParameters that can be registered via HTTP middleware * @return bool|ResolveResult true or an instance of ResolveResult if Route Part can resolve one or more $routeValues elements, otherwise false. */ diff --git a/Neos.Flow/Classes/Mvc/Routing/Route.php b/Neos.Flow/Classes/Mvc/Routing/Route.php index eb7998a333..6e37b61714 100644 --- a/Neos.Flow/Classes/Mvc/Routing/Route.php +++ b/Neos.Flow/Classes/Mvc/Routing/Route.php @@ -48,7 +48,7 @@ class Route /** * Default values * - * @var array + * @var array */ protected $defaults = []; @@ -91,7 +91,7 @@ class Route * Contains the routing results (indexed by "package", "controller" and * "action") after a successful call of matches() * - * @var array|null + * @var array|null */ protected $matchResults = []; @@ -112,7 +112,7 @@ class Route /** * The merged UriConstraints of all Route Parts after resolving * - * @var UriConstraints|null + * @var ?UriConstraints */ protected $resolvedUriConstraints; @@ -134,7 +134,7 @@ class Route * Contains associative array of Route Part options * (key: Route Part name, value: array of Route Part options) * - * @var array + * @var array> */ protected $routePartsConfiguration = []; @@ -172,9 +172,22 @@ class Route */ protected $routeValuesNormalizer; + /** + * @param array{ + * name?: string, + * uriPattern: string, + * defaults?: array, + * routeParts?: array, + * toLowerCase?: bool, + * appendExceedingArguments?: bool, + * cache?: array{lifetime?: int, tags?: array}, + * httpMethods?: list + * } $configuration + * @return static + */ public static function fromConfiguration(array $configuration): static { - /** @phpstan-ignore-next-line phpstan doesn't respekt the consistent constructor flag in the class doc block */ + /** @phpstan-ignore new.static */ $route = new static(); if (isset($configuration['name'])) { $route->setName($configuration['name']); @@ -232,7 +245,7 @@ public function getName() * Sets default values for this Route. * This array is merged with the actual matchResults when match() is called. * - * @param array $defaults + * @param array $defaults * @return void */ public function setDefaults(array $defaults) @@ -243,7 +256,7 @@ public function setDefaults(array $defaults) /** * Returns default values for this Route. * - * @return array Route defaults + * @return array Route defaults */ public function getDefaults() { @@ -255,13 +268,9 @@ public function getDefaults() * * @param string $uriPattern * @return void - * @throws \InvalidArgumentException */ - public function setUriPattern($uriPattern) + public function setUriPattern(string $uriPattern) { - if (!is_string($uriPattern)) { - throw new \InvalidArgumentException(sprintf('URI Pattern must be of type string, %s given.', gettype($uriPattern)), 1223499724); - } $this->uriPattern = $uriPattern; $this->isParsed = false; } @@ -334,7 +343,7 @@ public function getAppendExceedingArguments() * Usage: setRoutePartsConfiguration(array('@controller' => * array('handler' => \Neos\Package\Subpackage\MyRoutePartHandler::class))); * - * @param array $routePartsConfiguration Route Parts configuration options + * @param array> $routePartsConfiguration Route Parts configuration options * @return void */ public function setRoutePartsConfiguration(array $routePartsConfiguration) @@ -345,7 +354,7 @@ public function setRoutePartsConfiguration(array $routePartsConfiguration) /** * Returns the route parts configuration of this route * - * @return array $routePartsConfiguration + * @return array> $routePartsConfiguration */ public function getRoutePartsConfiguration() { @@ -356,7 +365,7 @@ public function getRoutePartsConfiguration() * Limits the HTTP verbs that are accepted by this route. * If empty all HTTP verbs are accepted * - * @param array $httpMethods non-associative array in the format array('GET', 'POST', ...) + * @param list $httpMethods non-associative array in the format array('GET', 'POST', ...) * @return void */ public function setHttpMethods(array $httpMethods) @@ -365,7 +374,7 @@ public function setHttpMethods(array $httpMethods) } /** - * @return array + * @return list */ public function getHttpMethods() { @@ -405,7 +414,7 @@ public function hasHttpMethodConstraints() /** * Returns an array with the Route match results. * - * @return array An array of Route Parts and their values for further handling by the Router + * @return ?array An array of Route Parts and their values for further handling by the Router * @see \Neos\Flow\Mvc\Routing\Router */ public function getMatchResults() @@ -436,7 +445,7 @@ public function getMatchedLifetime(): ?RouteLifetime /** * Returns the merged UriConstraints of all Route Parts after resolving, or NULL if no constraints were set yet * - * @return UriConstraints|null + * @return ?UriConstraints */ public function getResolvedUriConstraints() { @@ -471,6 +480,7 @@ public function getResolvedLifetime(): ?RouteLifetime * @param RouteContext $routeContext The Route Context containing the current HTTP request object and, optional, Routing RouteParameters * @return boolean true if this Route corresponds to the given $routeContext, otherwise false * @throws InvalidRoutePartValueException + * @phpstan-assert-if-true array $this->getMatchResults() * @see getMatchResults() */ public function matches(RouteContext $routeContext) @@ -516,9 +526,11 @@ public function matches(RouteContext $routeContext) $routeMatches = true; $routePartValue = $matchResult->getMatchedValue(); if ($matchResult->hasTags()) { + /** @phpstan-ignore method.nonObject (already set) */ $this->matchedTags = $this->matchedTags->merge($matchResult->getTags()); } if ($matchResult->hasLifetime()) { + /** @phpstan-ignore method.nonObject (already set) */ $this->matchedLifetime = $this->matchedLifetime->merge($matchResult->getLifetime()); } } else { @@ -539,7 +551,11 @@ public function matches(RouteContext $routeContext) if ($this->containsObject($routePartValue)) { throw new InvalidRoutePartValueException('RoutePart::getValue() must only return simple types after calling RoutePart::match(). RoutePart "' . get_class($routePart) . '" returned one or more objects in Route "' . $this->getName() . '".'); } - $matchResults = Arrays::setValueByPath($matchResults, $routePart->getName(), $routePartValue); + $routePartName = $routePart->getName(); + if ($routePartName === null) { + throw new \Exception('Missing name for route part', 1744323723); + } + $matchResults = Arrays::setValueByPath($matchResults, $routePartName, $routePartValue); } } if (strlen($routePath) > 0) { @@ -558,6 +574,7 @@ public function matches(RouteContext $routeContext) * * @param ResolveContext $resolveContext context for this resolve invokation * @return boolean true if this Route corresponds to the given $routeValues, otherwise false + * @phpstan-assert-if-true UriConstraints $this->getResolvedUriConstraints() * @throws InvalidRoutePartValueException */ public function resolves(ResolveContext $resolveContext) @@ -592,12 +609,15 @@ public function resolves(ResolveContext $resolveContext) $hasRoutePartValue = true; $routePartValue = $resolveResult->getResolvedValue(); if ($resolveResult->hasUriConstraints()) { + /** @phpstan-ignore method.nonObject (already set) */ $this->resolvedUriConstraints = $this->resolvedUriConstraints->merge($resolveResult->getUriConstraints()); } if ($resolveResult->hasTags()) { + /** @phpstan-ignore method.nonObject (already set) */ $this->resolvedTags = $this->resolvedTags->merge($resolveResult->getTags()); } if ($resolveResult->hasLifetime()) { + /** @phpstan-ignore method.nonObject (already set) */ $this->resolvedLifetime = $this->resolvedLifetime->merge($resolveResult->getLifetime()); } } else { @@ -619,7 +639,7 @@ public function resolves(ResolveContext $resolveContext) $requireOptionalRouteParts = false; continue; } - if ($hasRoutePartValue && strtolower($routePartValue) !== strtolower($routePartDefaultValue)) { + if ($hasRoutePartValue && strtolower($routePartValue ?: '') !== strtolower($routePartDefaultValue ?: '')) { $matchingOptionalUriPortion .= $routePartValue; $requireOptionalRouteParts = true; } else { @@ -648,10 +668,12 @@ public function resolves(ResolveContext $resolveContext) } $routeValues = $internalArguments; } + /** @phpstan-ignore method.nonObject (already set) */ $this->resolvedUriConstraints = $this->resolvedUriConstraints->withAddedQueryValues($routeValues); } if (!empty($resolvedUriPath)) { + /** @phpstan-ignore method.nonObject (already set) */ $this->resolvedUriConstraints = $this->resolvedUriConstraints->withPath($resolvedUriPath); } return true; @@ -664,8 +686,8 @@ public function resolves(ResolveContext $resolveContext) * If a value exists but is not equal to is corresponding default, * iteration is interrupted and false is returned. * - * @param array $defaults - * @param array $routeValues + * @param array $defaults + * @param array $routeValues * @return boolean false if one of the $routeValues is not equal to it's default value. Otherwise true */ protected function compareAndRemoveMatchingDefaultValues(array $defaults, array &$routeValues) @@ -700,8 +722,8 @@ protected function compareAndRemoveMatchingDefaultValues(array $defaults, array * Removes all internal arguments (prefixed with two underscores) from the given $arguments * and returns them as array * - * @param array $arguments - * @return array the internal arguments + * @param array $arguments + * @return array the internal arguments */ protected function extractInternalArguments(array &$arguments) { @@ -813,6 +835,9 @@ public function parse() /** @var DynamicRoutePartInterface $lastRoutePart */ $lastRoutePart->setSplitString($routePartName); } + break; + default: + continue 2; } $routePart->setName($routePartName); if ($currentRoutePartIsOptional) { diff --git a/Neos.Flow/Classes/Mvc/Routing/RoutePartInterface.php b/Neos.Flow/Classes/Mvc/Routing/RoutePartInterface.php index 1d5266651b..016a00efa1 100644 --- a/Neos.Flow/Classes/Mvc/Routing/RoutePartInterface.php +++ b/Neos.Flow/Classes/Mvc/Routing/RoutePartInterface.php @@ -34,7 +34,7 @@ public function setName($partName); /** * Returns name of the Route Part. * - * @return string + * @return ?string */ public function getName(); @@ -108,13 +108,13 @@ public function isLowerCase(); * Defines options for this Route Part. * Options can be used to enrich a route part with parameters or settings like case sensivitity. * - * @param array $options + * @param array $options * @return void */ public function setOptions(array $options); /** - * @return array options of this Route Part. + * @return array options of this Route Part. */ public function getOptions(); @@ -135,7 +135,7 @@ public function match(&$routePath); * removes resolved elements from $routeValues-Array. * This is why $routeValues has to be passed by reference. * - * @param array &$routeValues An array with key/value pairs to be resolved by Dynamic Route Parts. + * @param array &$routeValues An array with key/value pairs to be resolved by Dynamic Route Parts. * @return bool|ResolveResult true or an instance of ResolveResult if Route Part can resolve one or more $routeValues elements, otherwise false. */ public function resolve(array &$routeValues); diff --git a/Neos.Flow/Classes/Mvc/Routing/Router.php b/Neos.Flow/Classes/Mvc/Routing/Router.php index d72bcd6f6a..60c3a0a358 100644 --- a/Neos.Flow/Classes/Mvc/Routing/Router.php +++ b/Neos.Flow/Classes/Mvc/Routing/Router.php @@ -76,7 +76,7 @@ public function injectLogger(LoggerInterface $logger) * 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 or NULL if no route matched + * @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 @@ -109,7 +109,7 @@ public function route(RouteContext $routeContext): array * Returns the route that has been matched with the last route() call. * Returns NULL if no route matched or route() has not been called yet * - * @return Route + * @return ?Route */ public function getLastMatchedRoute() { @@ -151,7 +151,7 @@ public function resolve(ResolveContext $resolveContext): UriInterface * Returns the route that has been resolved with the last resolve() call. * Returns NULL if no route was found or resolve() has not been called yet * - * @return Route + * @return ?Route */ public function getLastResolvedRoute() { diff --git a/Neos.Flow/Classes/Mvc/Routing/RouterCachingService.php b/Neos.Flow/Classes/Mvc/Routing/RouterCachingService.php index e9d5c7e89e..335f87c1c5 100644 --- a/Neos.Flow/Classes/Mvc/Routing/RouterCachingService.php +++ b/Neos.Flow/Classes/Mvc/Routing/RouterCachingService.php @@ -65,6 +65,12 @@ class RouterCachingService /** * @Flow\InjectConfiguration("mvc.routes") + * @phpstan-var array, + * providerFactory?: class-string, + * providerOptions?: array, + * }> indexed by package key * @var array */ protected $routingSettings; @@ -72,7 +78,7 @@ class RouterCachingService /** * @param LoggerInterface $logger */ - public function injectLogger(LoggerInterface $logger) + public function injectLogger(LoggerInterface $logger): void { $this->logger = $logger; } @@ -93,7 +99,7 @@ public function initializeObject() * Checks the cache for the given RouteContext and returns the result or false if no matching ache entry was found * * @param RouteContext $routeContext - * @return array|false the cached route values or false if no cache entry was found + * @return array|false the cached route values or false if no cache entry was found */ public function getCachedMatchResults(RouteContext $routeContext) { @@ -109,7 +115,7 @@ public function getCachedMatchResults(RouteContext $routeContext) * Stores the $matchResults in the cache * * @param RouteContext $routeContext - * @param array $matchResults + * @param array $matchResults * @param RouteTags|null $matchedTags * @param RouteLifetime|null $matchedLifetime * @return void @@ -170,8 +176,8 @@ public function storeResolvedUriConstraints(ResolveContext $resolveContext, UriC /** * @param string $uriPath - * @param array $routeValues - * @return array + * @param array $routeValues + * @return array */ protected function generateRouteTags($uriPath, $routeValues) { @@ -259,8 +265,8 @@ protected function containsObject($subject) /** * Recursively converts objects in an array to their identifiers * - * @param array $routeValues the array to be processed - * @return array|null the modified array or NULL if $routeValues contain an object and its identifier could not be determined + * @param array $routeValues the array to be processed + * @return array|null the modified array or NULL if $routeValues contain an object and its identifier could not be determined */ protected function convertObjectsToHashes(array $routeValues) { @@ -289,7 +295,7 @@ protected function convertObjectsToHashes(array $routeValues) * Generates the Resolve cache identifier for the given Request * * @param ResolveContext $resolveContext - * @param array $routeValues + * @param array $routeValues * @return string */ protected function buildResolveCacheIdentifier(ResolveContext $resolveContext, array $routeValues) @@ -303,8 +309,8 @@ protected function buildResolveCacheIdentifier(ResolveContext $resolveContext, a * Helper method to generate tags by taking all UUIDs contained * in the given $routeValues or $matchResults * - * @param array $values - * @return array + * @param array $values + * @return array */ protected function extractUuids(array $values) { diff --git a/Neos.Flow/Classes/Mvc/Routing/RouterInterface.php b/Neos.Flow/Classes/Mvc/Routing/RouterInterface.php index 8aebb7e108..e2596510f0 100644 --- a/Neos.Flow/Classes/Mvc/Routing/RouterInterface.php +++ b/Neos.Flow/Classes/Mvc/Routing/RouterInterface.php @@ -26,7 +26,7 @@ interface RouterInterface * Returns the matchResults of the matching route. * * @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 * @throws NoMatchingRouteException if no route matched the $routeContext */ public function route(RouteContext $routeContext): array; diff --git a/Neos.Flow/Classes/Mvc/Routing/Routes.php b/Neos.Flow/Classes/Mvc/Routing/Routes.php index cc0822c971..66e14d2c94 100644 --- a/Neos.Flow/Classes/Mvc/Routing/Routes.php +++ b/Neos.Flow/Classes/Mvc/Routing/Routes.php @@ -21,7 +21,7 @@ final class Routes implements \IteratorAggregate private function __construct( Route ...$routes ) { - $this->routes = $routes; + $this->routes = array_values($routes); // validate that each route is unique $routesWithHttpMethodConstraints = []; @@ -46,6 +46,18 @@ public static function create(Route ...$routes): self return new self(...$routes); } + /** + * @param array, + * routeParts?: array, + * toLowerCase?: bool, + * appendExceedingArguments?: bool, + * cache?: array{lifetime?: int, tags?: array}, + * httpMethods?: list + * }> $configuration + */ public static function fromConfiguration(array $configuration): self { $routes = []; diff --git a/Neos.Flow/Classes/Mvc/Routing/StaticRoutePart.php b/Neos.Flow/Classes/Mvc/Routing/StaticRoutePart.php index 9e33c527f5..16970d5018 100644 --- a/Neos.Flow/Classes/Mvc/Routing/StaticRoutePart.php +++ b/Neos.Flow/Classes/Mvc/Routing/StaticRoutePart.php @@ -21,7 +21,7 @@ class StaticRoutePart extends \Neos\Flow\Mvc\Routing\AbstractRoutePart /** * Gets default value of the Route Part. * - * @return string + * @return ?string */ public function getDefaultValue() { @@ -37,7 +37,6 @@ public function getDefaultValue() */ public function match(&$routePath) { - $routePath = (string)$routePath; $this->value = null; if ($this->name === null || $this->name === '') { return false; @@ -57,7 +56,7 @@ public function match(&$routePath) /** * Sets the Route Part value to the Route Part name and returns true if successful. * - * @param array $routeValues not used but needed to implement \Neos\Flow\Mvc\Routing\AbstractRoutePart + * @param array $routeValues not used but needed to implement \Neos\Flow\Mvc\Routing\AbstractRoutePart * @return boolean */ public function resolve(array &$routeValues) diff --git a/Neos.Flow/Classes/Mvc/Routing/TestingRoutesProvider.php b/Neos.Flow/Classes/Mvc/Routing/TestingRoutesProvider.php index e20be3c46b..b7168b74da 100644 --- a/Neos.Flow/Classes/Mvc/Routing/TestingRoutesProvider.php +++ b/Neos.Flow/Classes/Mvc/Routing/TestingRoutesProvider.php @@ -36,7 +36,7 @@ public function __construct( * * @internal Please use {@see FunctionalTestCase::registerRoute} instead. */ - public function addRoute(Route $route) + public function addRoute(Route $route): void { // we prepended the route, like the old Router::addRoute $this->additionalRoutes = Routes::create($route)->merge($this->additionalRoutes); diff --git a/Neos.Flow/Classes/Mvc/Routing/UriBuilder.php b/Neos.Flow/Classes/Mvc/Routing/UriBuilder.php index c31ed58424..4600ad1c85 100644 --- a/Neos.Flow/Classes/Mvc/Routing/UriBuilder.php +++ b/Neos.Flow/Classes/Mvc/Routing/UriBuilder.php @@ -51,13 +51,13 @@ class UriBuilder protected $request; /** - * @var array + * @var array */ protected $arguments = []; /** * Arguments which have been used for building the last URI - * @var array + * @var array */ protected $lastArguments = []; @@ -78,7 +78,7 @@ class UriBuilder protected $addQueryString = false; /** - * @var array + * @var array * @deprecated with Flow 9.0 */ protected $argumentsToBeExcludedFromQueryString = []; @@ -117,7 +117,7 @@ public function getRequest() * If you want to "prefix" arguments, you can pass in multidimensional arrays: * array('prefix1' => array('foo' => 'bar')) gets "&prefix1[foo]=bar" * - * @param array $arguments + * @param array $arguments * @return UriBuilder the current UriBuilder to allow method chaining * @api */ @@ -128,7 +128,7 @@ public function setArguments(array $arguments) } /** - * @return array + * @return array * @api */ public function getArguments() @@ -228,7 +228,7 @@ public function getAddQueryString() * A list of arguments to be excluded from the query parameters * Only active if addQueryString is set * - * @param array $argumentsToBeExcludedFromQueryString + * @param array $argumentsToBeExcludedFromQueryString * @return UriBuilder the current UriBuilder to allow method chaining * @deprecated with Flow 9.0 */ @@ -239,7 +239,7 @@ public function setArgumentsToBeExcludedFromQueryString(array $argumentsToBeExcl } /** - * @return array + * @return array * @deprecated with Flow 9.0 */ public function getArgumentsToBeExcludedFromQueryString() @@ -251,7 +251,7 @@ public function getArgumentsToBeExcludedFromQueryString() * Returns the arguments being used for the last URI being built. * This is only set after build() / uriFor() has been called. * - * @return array The last arguments + * @return array The last arguments */ public function getLastArguments() { @@ -330,9 +330,9 @@ public function uriFor(string $actionName, array $controllerArguments = [], ?str * ) * ) * - * @param array $arguments arguments + * @param array $arguments arguments * @param ActionRequest $currentRequest - * @return array arguments with namespace + * @return array arguments with namespace */ protected function addNamespaceToArguments(array $arguments, ActionRequest $currentRequest) { @@ -349,7 +349,7 @@ protected function addNamespaceToArguments(array $arguments, ActionRequest $curr /** * Builds the URI * - * @param array $arguments optional URI arguments. Will be merged with $this->arguments with precedence to $arguments + * @param array $arguments optional URI arguments. Will be merged with $this->arguments with precedence to $arguments * @return string the (absolute or relative) URI as string * @throws \Neos\Flow\Http\Exception * @api @@ -389,8 +389,8 @@ public function build(array $arguments = []) * The request hierarchy is structured as follows: * root (HTTP) > main (Action) > sub (Action) > sub sub (Action) * - * @param array $arguments - * @return array + * @param array $arguments + * @return array */ protected function mergeArgumentsWithRequestArguments(array $arguments) { diff --git a/Neos.Flow/Classes/Mvc/View/AbstractView.php b/Neos.Flow/Classes/Mvc/View/AbstractView.php index 86a8bc08d4..8dbc80139e 100644 --- a/Neos.Flow/Classes/Mvc/View/AbstractView.php +++ b/Neos.Flow/Classes/Mvc/View/AbstractView.php @@ -31,7 +31,7 @@ abstract class AbstractView implements ViewInterface * ... * ) * - * @var array + * @var array */ protected $supportedOptions = []; @@ -39,13 +39,13 @@ abstract class AbstractView implements ViewInterface * The configuration options of this view * @see $supportedOptions * - * @var array + * @var array */ protected $options = []; /** * View variables and their values - * @var array + * @var array * @see assign() */ protected $variables = []; @@ -60,7 +60,7 @@ abstract class AbstractView implements ViewInterface /** * Factory method to create an instance with given options. * - * @param array $options + * @param array $options * @return static */ public static function createWithOptions(array $options): self @@ -71,7 +71,7 @@ public static function createWithOptions(array $options): self /** * Set default options based on the supportedOptions provided * - * @param array $options + * @param array $options * @throws Exception */ public function __construct(array $options = []) @@ -155,7 +155,7 @@ public function assign(string $key, mixed $value): self /** * Add multiple variables to $this->variables. * - * @param array $values array in the format array(key1 => value1, key2 => value2) + * @param array $values array in the format array(key1 => value1, key2 => value2) * @return $this for chaining * @api */ diff --git a/Neos.Flow/Classes/Mvc/View/JsonView.php b/Neos.Flow/Classes/Mvc/View/JsonView.php index a614500bee..22a6c15e47 100644 --- a/Neos.Flow/Classes/Mvc/View/JsonView.php +++ b/Neos.Flow/Classes/Mvc/View/JsonView.php @@ -40,7 +40,7 @@ class JsonView extends AbstractView /** * Supported options - * @var array + * @var array> */ protected $supportedOptions = [ 'jsonEncodingOptions' => [0, 'Bitmask of supported Encoding options. See https://php.net/manual/en/json.constants.php', 'integer'], @@ -66,7 +66,7 @@ class JsonView extends AbstractView /** * Only variables whose name is contained in this array will be rendered * - * @var array + * @var array */ protected $variablesToRender = ['value']; @@ -166,7 +166,7 @@ class JsonView extends AbstractView * {"customer":{"firstName":"John","__class":"Customer"}} * This might be of interest to not provide information about the package or domain structure behind. * - * @var array + * @var array */ protected $configuration = []; @@ -180,7 +180,7 @@ class JsonView extends AbstractView * Specifies which variables this JsonView should render * By default only the variable 'value' will be rendered * - * @param array $variablesToRender + * @param array $variablesToRender * @return void * @api */ @@ -190,7 +190,7 @@ public function setVariablesToRender(array $variablesToRender) } /** - * @param array $configuration The rendering configuration for this JSON view + * @param array $configuration The rendering configuration for this JSON view * @return void */ public function setConfiguration(array $configuration) @@ -220,7 +220,7 @@ public function render(): ResponseInterface * Loads the configuration and transforms the value to a serializable * array. * - * @return array|string|int|float|null An array containing the values, ready to be JSON encoded + * @return array|string|int|float|null An array containing the values, ready to be JSON encoded * @api */ protected function renderArray() @@ -244,12 +244,12 @@ protected function renderArray() * supplied configuration. * * @param mixed $value The value to transform - * @param array $configuration Configuration for transforming the value - * @return array|string|int|float|null The transformed value + * @param array $configuration Configuration for transforming the value + * @return array|string|int|float|null The transformed value */ protected function transformValue($value, array $configuration) { - if (is_array($value) || $value instanceof \ArrayAccess) { + if (is_iterable($value)) { $array = []; foreach ($value as $key => $element) { if (isset($configuration['_descendAll']) && is_array($configuration['_descendAll'])) { @@ -279,8 +279,8 @@ protected function transformValue($value, array $configuration) * array structure. * * @param object $object Object to traverse - * @param array $configuration Configuration for transforming the given object - * @return array|string Object structure as an array + * @param array $configuration Configuration for transforming the given object + * @return array|string Object structure as an array */ protected function transformObject($object, array $configuration) { diff --git a/Neos.Flow/Classes/Mvc/View/SimpleTemplateView.php b/Neos.Flow/Classes/Mvc/View/SimpleTemplateView.php index 5454cea8e8..77a853b856 100644 --- a/Neos.Flow/Classes/Mvc/View/SimpleTemplateView.php +++ b/Neos.Flow/Classes/Mvc/View/SimpleTemplateView.php @@ -25,7 +25,7 @@ class SimpleTemplateView extends AbstractView use StreamFactoryTrait; /** - * @var array + * @var array> */ protected $supportedOptions = [ 'templateSource' => ['', 'Source of the template to render', 'string'], diff --git a/Neos.Flow/Classes/Mvc/ViewConfigurationManager.php b/Neos.Flow/Classes/Mvc/ViewConfigurationManager.php index 3ea598ec72..3079b8b8e1 100644 --- a/Neos.Flow/Classes/Mvc/ViewConfigurationManager.php +++ b/Neos.Flow/Classes/Mvc/ViewConfigurationManager.php @@ -53,7 +53,11 @@ class ViewConfigurationManager * an array of options that will be set on the view object. * * @param ActionRequest $request - * @return array + * @return array{ + * requestFilter?: string, + * viewObjectName?: string, + * options?: array + * } */ public function getViewConfiguration(ActionRequest $request) { @@ -61,6 +65,13 @@ public function getViewConfiguration(ActionRequest $request) $viewConfiguration = $this->cache->get($cacheIdentifier); if ($viewConfiguration === false) { + /** + * @var array + * }> $configurations + */ $configurations = $this->configurationManager->getConfiguration('Views'); $requestMatcher = new RequestMatcher($request); diff --git a/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php b/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php index 8f678fe7ee..e04e6513db 100644 --- a/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php +++ b/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php @@ -17,6 +17,7 @@ use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException; use Neos\Flow\ObjectManagement\Configuration\Configuration; +use Neos\Flow\ObjectManagement\Configuration\ConfigurationArgument; use Neos\Flow\ObjectManagement\Configuration\ConfigurationBuilder; use Neos\Flow\ObjectManagement\Configuration\ConfigurationProperty as Property; use Neos\Flow\Annotations as Flow; @@ -25,6 +26,7 @@ use Neos\Flow\ObjectManagement\Exception\WrongScopeException; use Neos\Flow\Package\FlowPackageInterface; use Neos\Flow\Package\PackageInterface; +use Neos\Flow\Package\PackageKeyAwareInterface; use Neos\Flow\Reflection\ReflectionService; use Psr\Log\LoggerInterface; @@ -33,6 +35,8 @@ * singleton scoped objects. This Object Manager is used during compile time when the proxy * class based DI mechanism is not yet available. * + * @template ObjectRecordInstance of object + * @extends ObjectManager * @Flow\Scope("singleton") * @Flow\Proxy(false) */ @@ -59,24 +63,24 @@ class CompileTimeObjectManager extends ObjectManager protected ?LoggerInterface $logger; /** - * @var array + * @var array */ protected array $objectConfigurations = []; /** * A list of all class names known to the Object Manager * - * @var array + * @var array> */ protected array $registeredClassNames = []; /** - * @var array + * @var array */ protected array $objectNameBuildStack = []; /** - * @var array + * @var array> */ protected array $cachedClassNamesByScope = []; @@ -123,7 +127,7 @@ public function injectLogger(LoggerInterface $logger): void /** * Initializes the object configurations and some other parts of this Object Manager. * - * @param PackageInterface[] $packages An array of active packages to consider + * @param array $packages An array of active packages to consider * @return void * @throws InvalidConfigurationTypeException * @throws InvalidObjectConfigurationException @@ -132,17 +136,46 @@ public function injectLogger(LoggerInterface $logger): void public function initialize(array $packages): void { $this->registeredClassNames = $this->registerClassFiles($packages); - $this->reflectionService->buildReflectionData($this->registeredClassNames); - - $rawCustomObjectConfigurations = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_OBJECTS); + $this->reflectionService?->buildReflectionData($this->registeredClassNames); + + /** + * @var array, + * properties?: array + * } + * }>, + * }>> $rawCustomObjectConfigurations + */ + $rawCustomObjectConfigurations = $this->configurationManager?->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_OBJECTS) ?: []; $configurationBuilder = new ConfigurationBuilder(); - $configurationBuilder->injectReflectionService($this->reflectionService); - $configurationBuilder->injectLogger($this->logger); - $configurationBuilder->injectExcludeClassesFromConstructorAutowiring($this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Neos.Flow.object.dependencyInjection.excludeClassesFromConstructorAutowiring')); - - $this->objectConfigurations = $configurationBuilder->buildObjectConfigurations($this->registeredClassNames, $rawCustomObjectConfigurations); - + if ($this->reflectionService) { + $configurationBuilder->injectReflectionService($this->reflectionService); + } + if ($this->logger) { + $configurationBuilder->injectLogger($this->logger); + } + $configurationBuilder->injectExcludeClassesFromConstructorAutowiring( + $this->configurationManager?->getConfiguration( + ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, + 'Neos.Flow.object.dependencyInjection.excludeClassesFromConstructorAutowiring' + ) ?: [] + ); + + $this->objectConfigurations = $configurationBuilder->buildObjectConfigurations( + $this->registeredClassNames, + $rawCustomObjectConfigurations + ); + + /** @phpstan-ignore argument.type (@todo revisit array shapes) */ $this->setObjects($this->buildObjectsArray()); } @@ -162,6 +195,7 @@ public function initialize(array $packages): void public function setInstance($objectName, $instance): void { if ($this->registeredClassNames === []) { + /** @phpstan-ignore assign.propertyType (classical subtype problem) */ $this->objects[$objectName][self::KEY_INSTANCE] = $instance; } else { parent::setInstance($objectName, $instance); @@ -171,7 +205,7 @@ public function setInstance($objectName, $instance): void /** * Returns a list of all class names, grouped by package key, which were registered by registerClassFiles() * - * @return array + * @return array> */ public function getRegisteredClassNames(): array { @@ -182,7 +216,7 @@ public function getRegisteredClassNames(): array * Returns a list of class names, which are configured with the given scope * * @param integer $scope One of the ObjectConfiguration::SCOPE_ constants - * @return array An array of class names configured with the given scope + * @return array An array of class names configured with the given scope */ public function getClassNamesByScope(int $scope): array { @@ -192,11 +226,13 @@ public function getClassNamesByScope(int $scope): array if (isset($information[self::KEY_CLASS_NAME])) { $this->cachedClassNamesByScope[$scope][] = $information[self::KEY_CLASS_NAME]; } else { + /** @phpstan-ignore assign.propertyType (I guess this is a class name here) */ $this->cachedClassNamesByScope[$scope][] = $objectName; } } } } + /** @phpstan-ignore return.type (@todo check class-string vs. string) */ return $this->cachedClassNamesByScope[$scope]; } @@ -207,8 +243,8 @@ public function getClassNamesByScope(int $scope): array * * For performance reasons this function ignores classes whose name ends with "Exception". * - * @param array $packages A list of packages to consider - * @return array A list of class names which were discovered in the given packages + * @param array $packages A list of packages to consider + * @return array> A list of class names which were discovered in the given packages * * @throws InvalidConfigurationTypeException */ @@ -222,7 +258,9 @@ protected function registerClassFiles(array $packages): array $includeClassesConfiguration = $this->allSettings['Neos']['Flow']['object']['includeClasses']; } + /** @var array> $includeClassesConfiguration */ + /** @var array> $availableClassNames */ $availableClassNames = ['' => ['DateTime']]; $shouldRegisterFunctionalTestClasses = (bool)($this->allSettings['Neos']['Flow']['object']['registerFunctionalTestClasses'] ?? false); @@ -242,6 +280,7 @@ protected function registerClassFiles(array $packages): array } } } + /** @phpstan-ignore booleanAnd.rightAlwaysTrue (can also be something else) */ if (isset($availableClassNames[$packageKey]) && is_array($availableClassNames[$packageKey])) { $availableClassNames[$packageKey] = array_unique($availableClassNames[$packageKey]); } @@ -254,9 +293,9 @@ protected function registerClassFiles(array $packages): array * Given an array of class names by package key this filters out classes that * have been configured to be included by object management. * - * @param array $classNames 2-level array - key of first level is package key, value of second level is classname (FQN) - * @param array $includeClassesConfiguration array of includeClasses configurations - * @return array The input array with all configured to be included in object management added in + * @param array> $classNames 2-level array - key of first level is package key, value of second level is classname (FQN) + * @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 */ protected function filterClassNamesFromConfiguration(array $classNames, array $includeClassesConfiguration): array @@ -267,16 +306,15 @@ protected function filterClassNamesFromConfiguration(array $classNames, array $i /** * Filters the classnames available for object management by filter expressions that includes classes. * - * @param array $classNames All classnames per package - * @param array $filterConfiguration The filter configuration to apply - * @return array the remaining class - * @throws InvalidConfigurationTypeException + * @param array> $classNames All classnames per package + * @param array $filterConfiguration The filter configuration to apply + * @return array> the remaining class */ protected function applyClassFilterConfiguration(array $classNames, array $filterConfiguration): array { foreach ($filterConfiguration as $packageKey => $filterExpressions) { if (!array_key_exists($packageKey, $classNames)) { - $this->logger->debug('The package "' . $packageKey . '" specified in the setting "Neos.Flow.object.includeClasses" was either excluded or is not loaded.'); + $this->logger?->debug('The package "' . $packageKey . '" specified in the setting "Neos.Flow.object.includeClasses" was either excluded or is not loaded.'); continue; } if (!is_array($filterExpressions)) { @@ -310,13 +348,22 @@ static function ($className) use ($filterExpression) { * Builds the objects array which contains information about the registered objects, * their scope, class, built method etc. * - * @return array + * @return array, + * }> * @throws CacheException */ protected function buildObjectsArray(): array { $objects = []; - /* @var $objectConfiguration Configuration */ foreach ($this->objectConfigurations as $objectConfiguration) { $objectName = $objectConfiguration->getObjectName(); $objects[$objectName] = [ @@ -337,6 +384,9 @@ protected function buildObjectsArray(): array $factoryMethodArguments = $objectConfiguration->getFactoryArguments(); if (count($factoryMethodArguments) > 0) { foreach ($factoryMethodArguments as $index => $argument) { + if (!$argument instanceof ConfigurationArgument) { + continue; + } $objects[$objectName][self::KEY_FACTORY_ARGUMENTS][$index] = [ self::KEY_ARGUMENT_TYPE => $argument->getType(), self::KEY_ARGUMENT_VALUE => $argument->getValue() @@ -345,14 +395,14 @@ protected function buildObjectsArray(): array } } } - $this->configurationCache->set('objects', $objects); + $this->configurationCache?->set('objects', $objects); return $objects; } /** * Returns object configurations which were previously built by the ConfigurationBuilder. * - * @return array + * @return array */ public function getObjectConfigurations(): array { @@ -395,7 +445,6 @@ public function get($objectName, ...$constructorArguments): object $this->objectNameBuildStack[] = $objectName; $object = parent::get($objectName); - /** @var Configuration $objectConfiguration */ $objectConfiguration = $this->objectConfigurations[$objectName]; foreach ($objectConfiguration->getProperties() as $propertyName => $property) { if ($property->getAutowiring() !== Configuration::AUTOWIRING_MODE_ON) { @@ -407,6 +456,9 @@ public function get($objectName, ...$constructorArguments): object break; case Property::PROPERTY_TYPES_CONFIGURATION: $propertyValue = $property->getValue(); + if (!$this->configurationManager) { + throw new \Exception('Cannot resolve configuration property type without a configuration manager instance', 1744305311); + } $value = $this->configurationManager->getConfiguration($propertyValue['type'], $propertyValue['path']); break; case Property::PROPERTY_TYPES_OBJECT: diff --git a/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php b/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php index bb24986041..d5c055c589 100644 --- a/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php +++ b/Neos.Flow/Classes/ObjectManagement/Configuration/Configuration.php @@ -12,7 +12,6 @@ */ use Neos\Flow\Annotations as Flow; -use Neos\Flow\Configuration\Exception\InvalidConfigurationException; /** * Flow Object Configuration @@ -36,13 +35,13 @@ class Configuration /** * Name of the class the object is based on - * @var string $className + * @var class-string|"" $className */ protected $className; /** * Key of the package the specified object is part of - * @var string + * @var ?string */ protected $packageKey; @@ -60,7 +59,7 @@ class Configuration /** * Arguments of the factory method - * @var array + * @var array */ protected $factoryArguments = []; @@ -71,13 +70,13 @@ class Configuration /** * Arguments of the constructor detected by reflection - * @var array + * @var array */ protected $arguments = []; /** * Array of properties which are injected into the object - * @var array + * @var array */ protected $properties = []; @@ -109,7 +108,9 @@ class Configuration * The constructor * * @param string $objectName The unique identifier of the object - * @param string $className Name of the class which provides the functionality of this object + * @param ?class-string $className Name of the class which provides the functionality of this object + * @todo fix the weird test case that is the only thing ever putting null in $className, + * @see \Neos\Flow\Tests\Unit\ObjectManagement\Configuration\ConfigurationTest::setUp()) */ public function __construct($objectName, $className = null) { @@ -121,7 +122,9 @@ public function __construct($objectName, $className = null) } $this->objectName = $objectName; - $this->className = ($className === null ? $objectName : $className); + $className = ($className === null ? $objectName : $className); + /** @phpstan-ignore assign.propertyType (I guess object name is a class here) */ + $this->className = $className; } /** @@ -149,7 +152,7 @@ public function getObjectName() /** * Setter function for property "className" * - * @param string $className Name of the class which provides the functionality for this object + * @param class-string $className Name of the class which provides the functionality for this object * @return void */ public function setClassName($className) @@ -160,7 +163,7 @@ public function setClassName($className) /** * Returns the class name * - * @return string Name of the implementing class of this object + * @return class-string|"" Name of the implementing class of this object (or empty string for some reason) */ public function getClassName() { @@ -181,7 +184,7 @@ public function setPackageKey($packageKey) /** * Returns the package key * - * @return string Key of the package this object is part of + * @return ?string Key of the package this object is part of */ public function getPackageKey() { @@ -221,9 +224,9 @@ public function getFactoryObjectName() * @return void * @throws \InvalidArgumentException */ - public function setFactoryMethodName($methodName) + public function setFactoryMethodName(string $methodName) { - if (!is_string($methodName) || $methodName === '') { + if ($methodName === '') { throw new \InvalidArgumentException('No valid factory method name specified.', 1229700126); } $this->factoryMethodName = $methodName; @@ -337,8 +340,7 @@ public function getLifecycleShutdownMethodName() * Setter function for injection properties. If an empty array is passed to this * method, all (possibly) defined properties are removed from the configuration. * - * @param array $properties Array of ConfigurationProperty - * @throws InvalidConfigurationException + * @param array $properties Array of ConfigurationProperty * @return void */ public function setProperties(array $properties) @@ -347,11 +349,7 @@ public function setProperties(array $properties) $this->properties = []; } else { foreach ($properties as $value) { - if ($value instanceof ConfigurationProperty) { - $this->setProperty($value); - } else { - throw new InvalidConfigurationException(sprintf('Only ConfigurationProperty instances are allowed, "%s" given', is_object($value) ? get_class($value) : gettype($value)), 1449217567); - } + $this->setProperty($value); } } } @@ -382,7 +380,6 @@ public function setProperty(ConfigurationProperty $property) * method, all (possibly) defined constructor arguments are removed from the configuration. * * @param array $arguments - * @throws InvalidConfigurationException * @return void */ public function setArguments(array $arguments) @@ -391,11 +388,7 @@ public function setArguments(array $arguments) $this->arguments = []; } else { foreach ($arguments as $argument) { - if ($argument instanceof ConfigurationArgument) { - $this->setArgument($argument); - } else { - throw new InvalidConfigurationException(sprintf('Only ConfigurationArgument instances are allowed, "%s" given', is_object($argument) ? get_class($argument) : gettype($argument)), 1449217803); - } + $this->setArgument($argument); } } } @@ -429,7 +422,7 @@ public function getArguments() for ($index = 1; $index <= $argumentsCount; $index++) { $sortedArguments[$index] = $this->arguments[$index] ?? null; } - return $sortedArguments; + return array_filter($sortedArguments); } /** @@ -446,7 +439,7 @@ public function setFactoryArgument(ConfigurationArgument $argument) /** * Returns a sorted array of factory method arguments indexed by position (starting with "1") * - * @return array A sorted array of ConfigurationArgument objects with the argument position as index + * @return array A sorted array of ConfigurationArgument objects with the argument position as index */ public function getFactoryArguments() { diff --git a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationArgument.php b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationArgument.php index 89a1bb12f6..542f28b2a0 100644 --- a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationArgument.php +++ b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationArgument.php @@ -49,7 +49,7 @@ class ConfigurationArgument /** * Constructor - sets the index, value and type of the argument * - * @param string $index Index of the argument + * @param integer $index Index of the argument * @param mixed $value Value of the argument * @param integer $type Type of the argument - one of the argument_TYPE_* constants */ diff --git a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationBuilder.php b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationBuilder.php index f540adb8d0..77e97f42d4 100644 --- a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationBuilder.php +++ b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationBuilder.php @@ -48,7 +48,7 @@ class ConfigurationBuilder * An array of object names for which constructor injection autowiring should be disabled. * Note that the object names are regular expressions. * - * @var array + * @var array */ protected array $excludeClassesFromConstructorAutowiring = []; @@ -72,6 +72,9 @@ public function injectLogger(LoggerInterface $logger) $this->logger = $logger; } + /** + * @param array $excludeClassesFromConstructorAutowiring + */ public function injectExcludeClassesFromConstructorAutowiring(array $excludeClassesFromConstructorAutowiring): void { $this->excludeClassesFromConstructorAutowiring = $excludeClassesFromConstructorAutowiring; @@ -83,9 +86,22 @@ public function injectExcludeClassesFromConstructorAutowiring(array $excludeClas * into the overall configuration. Finally autowires dependencies of arguments and properties * which can be resolved automatically. * - * @param array $availableClassAndInterfaceNamesByPackage An array of available class names, grouped by package key - * @param array $rawObjectConfigurationsByPackages An array of package keys and their raw (ie. unparsed) object configurations - * @return array Object configurations + * @param array> $availableClassAndInterfaceNamesByPackage An array of available class names, grouped by package key + * @param array, + * properties?: array + * } + * }>, + * }|mixed>> $rawObjectConfigurationsByPackages An array of package keys and their raw (ie. unparsed) object configurations + * @return array Object configurations * @throws InvalidObjectConfigurationException */ public function buildObjectConfigurations(array $availableClassAndInterfaceNamesByPackage, array $rawObjectConfigurationsByPackages) @@ -102,7 +118,9 @@ public function buildObjectConfigurations(array $availableClassAndInterfaceNames } if (interface_exists($classOrInterfaceName)) { + /** @var class-string $interfaceName */ $interfaceName = $classOrInterfaceName; + /** @var class-string|false $implementationClassName */ $implementationClassName = $this->reflectionService->getDefaultImplementationClassNameForInterface($interfaceName); if (!isset($rawObjectConfigurationsByPackages[$packageKey][$interfaceName]) && $implementationClassName === false) { continue; @@ -160,6 +178,7 @@ public function buildObjectConfigurations(array $availableClassAndInterfaceNames } if (empty($newObjectConfiguration->getClassName()) && !$newObjectConfiguration->isCreatedByFactory()) { + /** @var class-string $objectName */ $count = count($this->reflectionService->getAllImplementationClassNamesForInterface($objectName)); $hint = ($count ? 'It seems like there is no class which implements that interface, maybe the object configuration is obsolete?' : sprintf('There are %s classes implementing that interface, therefore you must specify a specific class in your object configuration.', $count)); throw new InvalidObjectConfigurationException('The object configuration for "' . $objectName . '" in the object configuration of package "' . $packageKey . '" lacks a "className" entry. ' . $hint, 1422566751); @@ -193,9 +212,9 @@ public function buildObjectConfigurations(array $availableClassAndInterfaceNames * Builds a raw configuration array by parsing possible scope and autowiring * annotations from the given class or interface. * - * @param string $className - * @param array $rawObjectConfiguration - * @return array + * @param class-string $className + * @param array $rawObjectConfiguration + * @return array */ protected function enhanceRawConfigurationWithAnnotationOptions($className, array $rawObjectConfiguration): array { @@ -214,16 +233,17 @@ protected function enhanceRawConfigurationWithAnnotationOptions($className, arra * Builds an object configuration object from a generic configuration container. * * @param string $objectName Name of the object - * @param array $rawConfigurationOptions The configuration array with options for the object configuration + * @param array $rawConfigurationOptions The configuration array with options for the object configuration * @param string $configurationSourceHint A human readable hint on the original source of the configuration (for troubleshooting) - * @param Configuration $existingObjectConfiguration If set, this object configuration object will be used instead of creating a fresh one + * @param ?Configuration $existingObjectConfiguration If set, this object configuration object will be used instead of creating a fresh one * @return Configuration The object configuration object * @throws InvalidObjectConfigurationException if errors occurred during parsing */ protected function parseConfigurationArray($objectName, array $rawConfigurationOptions, $configurationSourceHint = '', $existingObjectConfiguration = null) { + /** @var class-string $className virtual objects without a className configuration throw an exception before */ $className = $rawConfigurationOptions['className'] ?? $objectName; - $objectConfiguration = ($existingObjectConfiguration instanceof Configuration) ? $existingObjectConfiguration : new Configuration($objectName, $className); + $objectConfiguration = $existingObjectConfiguration ?: new Configuration($objectName, $className); $objectConfiguration->setConfigurationSourceHint($configurationSourceHint); foreach ($rawConfigurationOptions as $optionName => $optionValue) { @@ -250,6 +270,9 @@ protected function parseConfigurationArray($objectName, array $rawConfigurationO case 'arguments': if (is_array($optionValue)) { foreach ($optionValue as $argumentName => $argumentValue) { + if (!is_int($argumentName)) { + throw new \Exception('arguments must be indexed with integers', 1744232239); + } if (array_key_exists('value', $argumentValue)) { $argument = new ConfigurationArgument($argumentName, $argumentValue['value'], ConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE); } elseif (array_key_exists('object', $argumentValue)) { @@ -268,6 +291,10 @@ protected function parseConfigurationArray($objectName, array $rawConfigurationO } break; case 'className': + $methodName = 'set' . ucfirst($optionName); + /** @phpstan-ignore argument.type (I guess the class name cannot be empty string here anymore) */ + $objectConfiguration->$methodName(trim((string)$optionValue)); + break; case 'factoryObjectName': case 'factoryMethodName': case 'lifecycleInitializationMethodName': @@ -346,6 +373,7 @@ protected function parsePropertyOfTypeObject($propertyName, $objectNameOrConfigu if (isset($objectNameOrConfiguration['factoryObjectName']) || isset($objectNameOrConfiguration['factoryMethodName'])) { $objectName = null; } else { + /** @phpstan-ignore argument.type (I guess we excluded "" as class name here already) */ $annotations = $this->reflectionService->getPropertyTagValues($parentObjectConfiguration->getClassName(), $propertyName, 'var'); if (count($annotations) !== 1) { throw new InvalidObjectConfigurationException(sprintf('Object %s (%s), for property "%s", contains neither object name, nor factory object name, and nor is the property properly @var - annotated.', $parentObjectConfiguration->getClassName(), $parentObjectConfiguration->getConfigurationSourceHint(), $propertyName), 1297097815); @@ -364,7 +392,7 @@ protected function parsePropertyOfTypeObject($propertyName, $objectNameOrConfigu /** * Parses the configuration for arguments of type OBJECT * - * @param string $argumentName Name of the argument + * @param int $argumentName index of the argument * @param mixed $objectNameOrConfiguration Value of the "object" section of the argument configuration - either a string or an array * @param string $configurationSourceHint A human readable hint on the original source of the configuration (for troubleshooting) * @return ConfigurationArgument A configuration argument of type object @@ -413,14 +441,12 @@ protected function parseArgumentOfTypeObject($argumentName, $objectNameOrConfigu * factoryObjectName: 'Some\Other\Factory\Class' * * - * @param array &$objectConfigurations + * @param array &$objectConfigurations * @return void */ protected function wireFactoryArguments(array &$objectConfigurations) { - /** @var Configuration $objectConfiguration */ foreach ($objectConfigurations as $objectConfiguration) { - /** @var ConfigurationArgument $argument */ foreach ($objectConfiguration->getFactoryArguments() as $index => $argument) { if ($argument === null || $argument->getType() !== ConfigurationArgument::ARGUMENT_TYPES_OBJECT) { continue; @@ -431,10 +457,7 @@ protected function wireFactoryArguments(array &$objectConfigurations) } $argumentObjectName = $objectConfiguration->getObjectName() . ':argument:' . $index; $argumentValue->setObjectName($argumentObjectName); - if ($argumentValue->getClassName() === null) { - $argumentValue->setClassName(''); - } - $objectConfigurations[$argumentObjectName] = $argument->getValue(); + $objectConfigurations[$argumentObjectName] = $argumentValue; $argument->set((int)$argument->getIndex(), $argumentObjectName, $argument->getType()); } } @@ -444,14 +467,13 @@ protected function wireFactoryArguments(array &$objectConfigurations) * If mandatory constructor arguments have not been defined yet, this function tries to autowire * them if possible. * - * @param array &$objectConfigurations + * @param array &$objectConfigurations * @return void * @throws UnresolvedDependenciesException */ protected function autowireArguments(array $objectConfigurations): void { foreach ($objectConfigurations as $objectConfiguration) { - /** @var Configuration $objectConfiguration */ $className = $objectConfiguration->getClassName(); if ($className === '') { continue; @@ -464,7 +486,6 @@ protected function autowireArguments(array $objectConfigurations): void continue; } - $className = $objectConfiguration->getClassName(); if (!$this->reflectionService->hasMethod($className, '__construct')) { continue; } @@ -491,6 +512,9 @@ protected function autowireArguments(array $objectConfigurations): void if ($injectConfigurationAnnotation->type !== ConfigurationManager::CONFIGURATION_TYPE_SETTINGS) { throw new InvalidObjectConfigurationException(sprintf('InjectConfiguration for constructor arguments currently only supports settings. Got type "%s" in constructor argument %s of class %s.', $injectConfigurationAnnotation->type, $index, $className), 1710409120); } + if (!$objectConfiguration->getPackageKey()) { + throw new \Exception('Missing package key'); + } $arguments[$index] = new ConfigurationArgument( $index, $injectConfigurationAnnotation->getFullConfigurationPath($objectConfiguration->getPackageKey()), @@ -505,7 +529,7 @@ protected function autowireArguments(array $objectConfigurations): void } elseif ($parameterInformation['allowsNull'] === true) { $arguments[$index] = new ConfigurationArgument($index, null, ConfigurationArgument::ARGUMENT_TYPES_STRAIGHTVALUE); $arguments[$index]->setAutowiring(Configuration::AUTOWIRING_MODE_OFF); - } elseif (interface_exists($parameterInformation['class'])) { + } elseif (interface_exists($parameterInformation['class'] ?? '')) { $debuggingHint = sprintf('No default implementation for the required interface %s was configured, therefore no specific class name could be used for this dependency. ', $parameterInformation['class']); } } @@ -522,13 +546,12 @@ protected function autowireArguments(array $objectConfigurations): void /** * This function tries to find yet unmatched dependencies which need to be injected via "inject*" setter methods. * - * @param array &$objectConfigurations + * @param array &$objectConfigurations * @return void * @throws ObjectException if an injected property is private */ protected function autowireProperties(array &$objectConfigurations) { - /** @var Configuration $objectConfiguration */ foreach ($objectConfigurations as $objectConfiguration) { $className = $objectConfiguration->getClassName(); $properties = $objectConfiguration->getProperties(); @@ -536,6 +559,7 @@ protected function autowireProperties(array &$objectConfigurations) if ($className === '') { continue; } + if ($objectConfiguration->getAutowiring() === Configuration::AUTOWIRING_MODE_OFF) { continue; } @@ -545,13 +569,6 @@ protected function autowireProperties(array &$objectConfigurations) } catch (\TypeError $error) { throw new UnknownClassException(sprintf('The class "%s" defined in the object configuration for object "%s", defined in package: %s, does not exist.', $className, $objectConfiguration->getObjectName(), $objectConfiguration->getPackageKey()), 1352371372); } - if (!is_array($classMethodNames)) { - if (!class_exists($className)) { - throw new UnknownClassException(sprintf('The class "%s" defined in the object configuration for object "%s", defined in package: %s, does not exist.', $className, $objectConfiguration->getObjectName(), $objectConfiguration->getPackageKey()), 1352371371); - } else { - throw new UnknownClassException(sprintf('Could not autowire properties of class "%s" because names of methods contained in that class could not be retrieved using get_class_methods().', $className), 1352386418); - } - } foreach ($classMethodNames as $methodName) { if (isset($methodName[6]) && strpos($methodName, 'inject') === 0 && $methodName[6] === strtoupper($methodName[6])) { $propertyName = lcfirst(substr($methodName, 6)); @@ -620,6 +637,9 @@ protected function autowireProperties(array &$objectConfigurations) } /** @var InjectConfiguration $injectConfigurationAnnotation */ $injectConfigurationAnnotation = $this->reflectionService->getPropertyAnnotation($className, $propertyName, InjectConfiguration::class); + if (!$objectConfiguration->getPackageKey()) { + throw new \Exception('Missing package key', 1744231440); + } $properties[$propertyName] = new ConfigurationProperty( $propertyName, [ diff --git a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationProperty.php b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationProperty.php index 75b2b19b60..ad9d39e4fb 100644 --- a/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationProperty.php +++ b/Neos.Flow/Classes/ObjectManagement/Configuration/ConfigurationProperty.php @@ -42,7 +42,7 @@ class ConfigurationProperty /** * If specified, this configuration is used for instantiating / retrieving an property of type object - * @var Configuration + * @var ?Configuration */ protected $objectConfiguration = null; diff --git a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxy.php b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxy.php index 6c219ba83b..37f05a6fd6 100644 --- a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxy.php +++ b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/DependencyProxy.php @@ -32,7 +32,7 @@ class DependencyProxy protected $builder; /** - * @var array + * @var array */ protected $propertyVariables = []; @@ -91,7 +91,7 @@ public function _addPropertyVariable(&$propertyVariable) * and returns the result of a call to the original method in the dependency * * @param string $methodName Name of the method to be called - * @param array $arguments An array of arguments to be passed to the method + * @param array $arguments An array of arguments to be passed to the method * @return mixed */ public function __call($methodName, array $arguments) diff --git a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/PropertyInjectionTrait.php b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/PropertyInjectionTrait.php index 36e0663fc6..02e18a8a50 100644 --- a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/PropertyInjectionTrait.php +++ b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/PropertyInjectionTrait.php @@ -13,6 +13,7 @@ /** * Boilerplate code for dependency injection + * @phpstan-ignore trait.unused (probably API) */ trait PropertyInjectionTrait { diff --git a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php index 62165d5fcf..d67ab987de 100644 --- a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php +++ b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php @@ -35,6 +35,8 @@ /** * A Proxy Class Builder which integrates Dependency Injection + * + * @template ObjectRecordInstance of object */ #[Flow\Scope("singleton")] #[Flow\Proxy(false)] @@ -47,10 +49,14 @@ class ProxyClassBuilder protected LoggerInterface $logger; protected ConfigurationManager $configurationManager; protected CacheManager $cacheManager; + + /** + * @phpstan-var CompileTimeObjectManager $objectManager + */ protected CompileTimeObjectManager $objectManager; /** - * @var array + * @var array */ protected array $objectConfigurations = []; @@ -80,6 +86,9 @@ public function injectLogger(LoggerInterface $logger): void $this->logger = $logger; } + /** + * @phpstan-param CompileTimeObjectManager $objectManager + */ public function injectObjectManager(CompileTimeObjectManager $objectManager): void { $this->objectManager = $objectManager; @@ -100,7 +109,8 @@ public function build(): void foreach ($this->objectConfigurations as $objectName => $objectConfiguration) { $className = $objectConfiguration->getClassName(); - if ($className === '' + if ( + $className === '' || $objectName !== $className || $this->compiler->hasCacheEntryForClass($className) === true || $this->reflectionService->isClassAbstract($className) @@ -210,6 +220,7 @@ protected function buildSetInstanceCode(Configuration $objectConfiguration): str */ protected function buildSerializeRelatedEntitiesCode(Configuration $objectConfiguration): string { + /** @var class-string $className (trust me, bro)*/ $className = $objectConfiguration->getClassName(); if ($this->reflectionService->hasMethod($className, '__sleep')) { @@ -271,6 +282,7 @@ protected function buildConstructorInjectionCode(Configuration $objectConfigurat $assignments = []; $argumentConfigurations = $objectConfiguration->getArguments(); + /** @phpstan-ignore argument.type (I guess we excluded "" as class name here already) */ $constructorParameterInfo = $this->reflectionService->getMethodParameters($objectConfiguration->getClassName(), '__construct'); $argumentNumberToOptionalInfo = []; @@ -280,9 +292,6 @@ protected function buildConstructorInjectionCode(Configuration $objectConfigurat $highestArgumentPositionWithAutowiringEnabled = -1; foreach ($argumentConfigurations as $argumentNumber => $argumentConfiguration) { - if (!$argumentConfiguration instanceof ConfigurationArgument) { - continue; - } $argumentPosition = $argumentNumber - 1; if ($argumentConfiguration->getAutowiring() === Configuration::AUTOWIRING_MODE_ON) { $highestArgumentPositionWithAutowiringEnabled = $argumentPosition; @@ -299,6 +308,7 @@ protected function buildConstructorInjectionCode(Configuration $objectConfigurat $doReturnCode = true; $argumentValueObjectName = $argumentValue->getObjectName(); $argumentValueClassName = $argumentValue->getClassName(); + /** @phpstan-ignore identical.alwaysFalse (@todo can this even be null?) */ if ($argumentValueClassName === null) { $preparedArgument = $this->buildCustomFactoryCall($argumentValue->getFactoryObjectName(), $argumentValue->getFactoryMethodName(), $argumentValue->getFactoryArguments()); $assignments[$argumentPosition] = $assignmentPrologue . $preparedArgument; @@ -368,7 +378,6 @@ protected function buildPropertyInjectionCode(Configuration $objectConfiguration $commands = []; $injectedProperties = []; foreach ($objectConfiguration->getProperties() as $propertyName => $propertyConfiguration) { - assert($propertyConfiguration instanceof ConfigurationProperty); if ($propertyConfiguration->getAutowiring() === Configuration::AUTOWIRING_MODE_OFF) { continue; } @@ -431,7 +440,7 @@ protected function buildPropertyInjectionCode(Configuration $objectConfiguration * @param Configuration $objectConfiguration Configuration of the object to inject into * @param string $propertyName Name of the property to inject * @param Configuration $propertyConfiguration Configuration of the object to inject - * @return array lines of PHP code + * @return array lines of PHP code * @throws UnknownObjectException */ protected function buildPropertyInjectionCodeByConfiguration(Configuration $objectConfiguration, $propertyName, Configuration $propertyConfiguration): array @@ -439,13 +448,19 @@ protected function buildPropertyInjectionCodeByConfiguration(Configuration $obje $className = $objectConfiguration->getClassName(); $propertyObjectName = $propertyConfiguration->getObjectName(); $propertyClassName = $propertyConfiguration->getClassName(); + /** @phpstan-ignore identical.alwaysFalse (@todo can this even be null?) */ if ($propertyClassName === null) { - $preparedSetterArgument = $this->buildCustomFactoryCall($propertyConfiguration->getFactoryObjectName(), $propertyConfiguration->getFactoryMethodName(), $propertyConfiguration->getFactoryArguments()); + $preparedSetterArgument = $this->buildCustomFactoryCall( + $propertyConfiguration->getFactoryObjectName(), + $propertyConfiguration->getFactoryMethodName(), + $propertyConfiguration->getFactoryArguments() + ); } else { - if (!is_string($propertyClassName) || !isset($this->objectConfigurations[$propertyClassName])) { + if (!isset($this->objectConfigurations[$propertyClassName])) { $configurationSource = $objectConfiguration->getConfigurationSourceHint(); throw new UnknownObjectException('Unknown class "' . $propertyClassName . '", specified as property "' . $propertyName . '" in the object configuration of object "' . $objectConfiguration->getObjectName() . '" (' . $configurationSource . ').', 1296130876); } + if ($this->objectConfigurations[$propertyClassName]->getScope() === Configuration::SCOPE_PROTOTYPE) { $preparedSetterArgument = 'new \\' . $propertyClassName . '(' . $this->buildMethodParametersCode($propertyConfiguration->getArguments()) . ')'; } else { @@ -453,6 +468,7 @@ protected function buildPropertyInjectionCodeByConfiguration(Configuration $obje } } + /** @phpstan-ignore argument.type (I guess we excluded "" as a class name here already) */ $result = $this->buildSetterInjectionCode($className, $propertyName, $preparedSetterArgument); if ($result !== null) { return $result; @@ -468,7 +484,7 @@ protected function buildPropertyInjectionCodeByConfiguration(Configuration $obje * @param ConfigurationProperty $propertyConfiguration * @param string $propertyName Name of the property to inject * @param string $propertyObjectName Object name of the object to inject - * @return array lines of PHP code + * @return array lines of PHP code * @throws UnknownObjectException */ public function buildPropertyInjectionCodeByString(Configuration $objectConfiguration, ConfigurationProperty $propertyConfiguration, $propertyName, $propertyObjectName): array @@ -491,6 +507,7 @@ public function buildPropertyInjectionCodeByString(Configuration $objectConfigur $preparedSetterArgument = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\'' . $propertyObjectName . '\')'; } + /** @phpstan-ignore argument.type (I guess we excluded "" as class name here already) */ $result = $this->buildSetterInjectionCode($className, $propertyName, $preparedSetterArgument); if ($result !== null) { return $result; @@ -511,7 +528,7 @@ public function buildPropertyInjectionCodeByString(Configuration $objectConfigur * @param string $propertyName Name of the property to inject * @param string $configurationType the configuration type of the injected property (one of the ConfigurationManager::CONFIGURATION_TYPE_* constants) * @param string|null $configurationPath Path with "." as separator specifying the setting value to inject or NULL if the complete configuration array should be injected - * @return array PHP code + * @return array PHP code */ public function buildPropertyInjectionCodeByConfigurationTypeAndPath(Configuration $objectConfiguration, $propertyName, $configurationType, $configurationPath = null): array { @@ -522,6 +539,7 @@ public function buildPropertyInjectionCodeByConfigurationTypeAndPath(Configurati $preparedSetterArgument = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\Neos\Flow\Configuration\ConfigurationManager::class)->getConfiguration(\'' . $configurationType . '\')'; } + /** @phpstan-ignore argument.type (I guess we excluded "" as class name here already) */ $result = $this->buildSetterInjectionCode($className, $propertyName, $preparedSetterArgument); if ($result !== null) { return $result; @@ -535,12 +553,13 @@ public function buildPropertyInjectionCodeByConfigurationTypeAndPath(Configurati * @param Configuration $objectConfiguration Configuration of the object to inject into * @param string $propertyName Name of the property to inject * @param string $cacheIdentifier the identifier of the cache to inject - * @return array PHP code + * @return array PHP code */ public function buildPropertyInjectionCodeByCacheIdentifier(Configuration $objectConfiguration, string $propertyName, string $cacheIdentifier): array { $className = $objectConfiguration->getClassName(); $preparedSetterArgument = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\Neos\Flow\Cache\CacheManager::class)->getCache(\'' . $cacheIdentifier . '\')'; + /** @phpstan-ignore argument.type (I guess we excluded "" as class name here already) */ $result = $this->buildSetterInjectionCode($className, $propertyName, $preparedSetterArgument); if ($result !== null) { return $result; @@ -555,7 +574,7 @@ public function buildPropertyInjectionCodeByCacheIdentifier(Configuration $objec * @param string $propertyClassName Class name of the dependency to inject * @param string $propertyName Name of the property in the class to inject into * @param string $preparedSetterArgument PHP code to use for retrieving the value to inject - * @return array PHP code + * @return array PHP code */ protected function buildLazyPropertyInjectionCode($propertyObjectName, $propertyClassName, $propertyName, $preparedSetterArgument): array { @@ -572,7 +591,7 @@ protected function buildLazyPropertyInjectionCode($propertyObjectName, $property * * If neither inject*() nor set*() exists, but the property does exist, NULL is returned * - * @param string $className Name of the class to inject into + * @param class-string $className Name of the class to inject into * @param string $propertyName Name of the property to inject * @param string $preparedSetterArgument PHP code to use for retrieving the value to inject * @@ -606,6 +625,7 @@ protected function buildSetterInjectionCode(string $className, string $propertyN protected function buildLifecycleInitializationCode(Configuration $objectConfiguration, int $cause): string { $lifecycleInitializationMethodName = $objectConfiguration->getLifecycleInitializationMethodName(); + /** @phpstan-ignore argument.type (I guess we excluded "" here already) */ if (!$this->reflectionService->hasMethod($objectConfiguration->getClassName(), $lifecycleInitializationMethodName)) { return ''; } @@ -634,6 +654,7 @@ protected function buildLifecycleInitializationCode(Configuration $objectConfigu protected function buildLifecycleShutdownCode(Configuration $objectConfiguration, int $cause): string { $lifecycleShutdownMethodName = $objectConfiguration->getLifecycleShutdownMethodName(); + /** @phpstan-ignore argument.type (I guess we excluded "" here already) */ if (!$this->reflectionService->hasMethod($objectConfiguration->getClassName(), $lifecycleShutdownMethodName)) { return ''; } @@ -656,7 +677,7 @@ protected function buildLifecycleShutdownCode(Configuration $objectConfiguration /** * FIXME: Not yet completely refactored to new proxy mechanism * - * @param array $argumentConfigurations + * @param array $argumentConfigurations * @return string * @throws InvalidConfigurationTypeException */ @@ -703,7 +724,7 @@ protected function buildMethodParametersCode(array $argumentConfigurations): str /** * @param string $customFactoryObjectName * @param string $customFactoryMethodName - * @param array $arguments + * @param array $arguments * @return string */ protected function buildCustomFactoryCall($customFactoryObjectName, $customFactoryMethodName, array $arguments): string @@ -715,7 +736,7 @@ protected function buildCustomFactoryCall($customFactoryObjectName, $customFacto /** * Compile the result of methods marked with CompileStatic into the proxy class * - * @param string $className + * @param class-string $className * @param ProxyClass $proxyClass * @return void * @throws ObjectException diff --git a/Neos.Flow/Classes/ObjectManagement/ObjectManager.php b/Neos.Flow/Classes/ObjectManagement/ObjectManager.php index 6a03f2c458..d76dac75a4 100644 --- a/Neos.Flow/Classes/ObjectManagement/ObjectManager.php +++ b/Neos.Flow/Classes/ObjectManagement/ObjectManager.php @@ -25,6 +25,17 @@ /** * Object Manager * + * @template ObjectRecordInstance of object + * @phpstan-type ObjectRecord array{ + * i?: ObjectRecordInstance, + * s: int, + * f?: array{0: string, 1: string}, + * fa?: array, + * c: class-string|null, + * p: string, + * l: string|null, + * } + * * @Flow\Scope("singleton") * @Flow\Proxy(false) */ @@ -50,12 +61,12 @@ class ObjectManager implements ObjectManagerInterface /** * An array of settings of all packages, indexed by package key * - * @var array + * @var array> */ protected array $allSettings = []; /** - * @var array + * @var array,ObjectRecord> */ protected array $objects = []; @@ -65,12 +76,12 @@ class ObjectManager implements ObjectManagerInterface protected array $dependencyProxies = []; /** - * @var array + * @var array */ protected array $classesBeingInstantiated = []; /** - * @var array + * @var array */ protected array $cachedLowerCasedObjectNames = []; @@ -78,7 +89,7 @@ class ObjectManager implements ObjectManagerInterface * A SplObjectStorage containing those objects which need to be shutdown when the container * shuts down. Each value of each entry is the respective shutdown method name. * - * @var \SplObjectStorage + * @var \SplObjectStorage */ protected \SplObjectStorage $shutdownObjects; @@ -86,7 +97,7 @@ class ObjectManager implements ObjectManagerInterface * A SplObjectStorage containing only those shutdown objects which have been registered for Flow. * These shutdown method will be called after all other shutdown methods have been called. * - * @var \SplObjectStorage + * @var \SplObjectStorage */ protected \SplObjectStorage $internalShutdownObjects; @@ -105,20 +116,22 @@ public function __construct(ApplicationContext $context) /** * Sets the objects array * - * @param array $objects An array of object names and some information about each registered object (scope, lower cased name etc.) + * @param array,ObjectRecord> $objects An array of object names and some information about each registered object (scope, lower cased name etc.) * @return void */ public function setObjects(array $objects): void { $this->objects = $objects; + /** @phpstan-ignore assign.propertyType (classical subtype problem) */ $this->objects[ObjectManagerInterface::class][self::KEY_INSTANCE] = $this; + /** @phpstan-ignore assign.propertyType (classical subtype problem) */ $this->objects[get_class($this)][self::KEY_INSTANCE] = $this; } /** * Injects the global settings array, indexed by package key. * - * @param array $settings The global settings + * @param array $settings The global settings * @return void * @Flow\Autowiring(false) */ @@ -215,6 +228,7 @@ public function get($objectName, ...$constructorArguments): object return $this->buildObjectByFactory($objectName); } + /** @phpstan-ignore assign.propertyType (classical subtype problem) */ $this->objects[$objectName][self::KEY_INSTANCE] = $this->buildObjectByFactory($objectName); return $this->objects[$objectName][self::KEY_INSTANCE]; } @@ -229,6 +243,7 @@ public function get($objectName, ...$constructorArguments): object return $this->instantiateClass($className, $constructorArguments); } + /** @phpstan-ignore assign.propertyType (classical subtype problem) */ $this->objects[$objectName][self::KEY_INSTANCE] = $this->instantiateClass($className, []); return $this->objects[$objectName][self::KEY_INSTANCE]; } @@ -314,7 +329,7 @@ public function getObjectNameByClassName($className): string|false * Returns the implementation class name for the specified object * * @param string $objectName The object name - * @return string|false The class name corresponding to the given object name or false if no such object is registered + * @return class-string|false The class name corresponding to the given object name or false if no such object is registered * @api */ public function getClassNameByObjectName($objectName): string|false @@ -322,6 +337,7 @@ public function getClassNameByObjectName($objectName): string|false if (!isset($this->objects[$objectName])) { return class_exists($objectName) ? $objectName : false; } + /** @phpstan-ignore return.type (@todo do a class check here) */ return $this->objects[$objectName][self::KEY_CLASS_NAME] ?? $objectName; } @@ -329,7 +345,7 @@ public function getClassNameByObjectName($objectName): string|false * Returns the key of the package the specified object is contained in. * * @param string $objectName The object name - * @return string|false The package key or false if no such object exists + * @return string|false The package key or false if no such object exists or null if no key was assigned * @internal */ public function getPackageKeyByObjectName($objectName): string|false @@ -361,6 +377,7 @@ public function setInstance($objectName, $instance): void 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); } + /** @phpstan-ignore assign.propertyType (classical subtype problem) */ $this->objects[$objectName][self::KEY_INSTANCE] = $instance; } @@ -451,7 +468,7 @@ public function forgetInstance($objectName): void /** * Returns all instances of objects with scope session * - * @return array + * @return array */ public function getSessionInstances(): array { @@ -493,7 +510,15 @@ public function shutdown(): void * Returns all current object configurations. * For internal use in bootstrap only. Can change anytime. * - * @return array + * @return array,array{ + * i?: ObjectRecordInstance, + * s: int, + * f?: array{0: string, 1: string}, + * fa?: array, + * c: ?class-string, + * p: ?string, + * l: ?string, + * }> */ public function getAllObjectConfigurations(): array { @@ -513,11 +538,12 @@ public function getAllObjectConfigurations(): array */ protected function buildObjectByFactory(string $objectName): object { - $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]; + $factoryName = $this->objects[$objectName][self::KEY_FACTORY][0] ?? null; + $factory = $factoryName ? $this->get($factoryName) : null; + $factoryMethodName = $this->objects[$objectName][self::KEY_FACTORY][1] ?? null; $factoryMethodArguments = []; - foreach ($this->objects[$objectName][self::KEY_FACTORY_ARGUMENTS] as $index => $argumentInformation) { + 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[self::KEY_ARGUMENT_VALUE]); @@ -535,14 +561,17 @@ protected function buildObjectByFactory(string $objectName): object return $factory->$factoryMethodName(...$factoryMethodArguments); } + if (!is_callable($factoryMethodName)) { + throw new \Exception($factoryMethodName . ' is not callable', 1744233428); + } return $factoryMethodName(...$factoryMethodArguments); } /** * Speed optimized alternative to ReflectionClass::newInstanceArgs() * - * @param string $className Name of the class to instantiate - * @param array $arguments Arguments to pass to the constructor + * @param class-string $className Name of the class to instantiate + * @param array $arguments Arguments to pass to the constructor * @return object The object * @throws Exception\CannotBuildObjectException * @throws \Exception @@ -566,7 +595,7 @@ protected function instantiateClass(string $className, array $arguments): object /** * Executes the methods of the provided objects. * - * @param \SplObjectStorage $shutdownObjects + * @param \SplObjectStorage $shutdownObjects * @return void */ protected function callShutdownMethods(\SplObjectStorage $shutdownObjects): void diff --git a/Neos.Flow/Classes/ObjectManagement/ObjectManagerInterface.php b/Neos.Flow/Classes/ObjectManagement/ObjectManagerInterface.php index c3ce693c72..1f9a7bf3c2 100644 --- a/Neos.Flow/Classes/ObjectManagement/ObjectManagerInterface.php +++ b/Neos.Flow/Classes/ObjectManagement/ObjectManagerInterface.php @@ -110,7 +110,7 @@ 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 class-string|false The class name corresponding to the given object name or false if no such object is registered * @api */ public function getClassNameByObjectName($objectName); @@ -119,7 +119,7 @@ public function getClassNameByObjectName($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|null The package key or false if no such object exists or null if no key was assigned */ public function getPackageKeyByObjectName($objectName); @@ -155,7 +155,7 @@ public function forgetInstance($objectName); /** * Returns all instances of objects with scope session * - * @return array + * @return array */ public function getSessionInstances(); diff --git a/Neos.Flow/Classes/ObjectManagement/Proxy/Compiler.php b/Neos.Flow/Classes/ObjectManagement/Proxy/Compiler.php index b54c92cd33..390a47cf9e 100644 --- a/Neos.Flow/Classes/ObjectManagement/Proxy/Compiler.php +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/Compiler.php @@ -38,6 +38,7 @@ class Compiler /** * @var CompileTimeObjectManager + * @phpstan-ignore missingType.generics (todo: learn how to properly do this) */ protected $objectManager; @@ -52,13 +53,13 @@ class Compiler protected $reflectionService; /** - * @var array + * @var array */ protected $proxyClasses = []; /** * Hardcoded list of Flow sub packages which must be immune proxying for security, technical or conceptual reasons. - * @var array + * @var array */ protected $excludedSubPackages = ['Neos\Flow\Aop', 'Neos\Flow\Cor', 'Neos\Flow\Obj', 'Neos\Flow\Pac', 'Neos\Flow\Ref', 'Neos\Flow\Uti']; @@ -73,7 +74,7 @@ class Compiler /** * The final map of proxy classes that end up in the cache. * - * @var array + * @var array */ protected $storedProxyClasses = []; @@ -86,8 +87,7 @@ public function __construct() } /** - * @param CompileTimeObjectManager $objectManager - * @return void + * @phpstan-ignore missingType.generics (todo: learn how to do this properly) */ public function injectObjectManager(CompileTimeObjectManager $objectManager): void { @@ -128,7 +128,7 @@ public function injectReflectionService(ReflectionService $reflectionService): v */ public function getProxyClass(string $fullClassName): ProxyClass|false { - if (interface_exists($fullClassName) || (class_exists(BaseTestCase::class) && in_array(BaseTestCase::class, class_parents($fullClassName), true))) { + if (interface_exists($fullClassName) || (class_exists(BaseTestCase::class) && in_array(BaseTestCase::class, class_parents($fullClassName) ?: [], true))) { return false; } @@ -141,7 +141,7 @@ public function getProxyClass(string $fullClassName): ProxyClass|false return false; } - if (method_exists($classReflection, 'isEnum') && $classReflection->isEnum()) { + if ($classReflection->isEnum()) { return false; } @@ -153,7 +153,7 @@ public function getProxyClass(string $fullClassName): ProxyClass|false return false; } // Annotation classes (like \Neos\Flow\Annotations\Entity) must never be proxied because that would break the Doctrine AnnotationParser - if ($classReflection->isFinal() && preg_match('/^\s?\*\s?\@Annotation\s/m', $classReflection->getDocComment()) === 1) { + if ($classReflection->isFinal() && $classReflection->getDocComment() && preg_match('/^\s?\*\s?\@Annotation\s/m', $classReflection->getDocComment()) === 1) { return false; } @@ -169,7 +169,7 @@ public function getProxyClass(string $fullClassName): ProxyClass|false * the proxy class doesn't have to be rebuilt because otherwise the cache would have been flushed by the file * monitor or some other mechanism. * - * @param string $fullClassName Name of the original class + * @param class-string $fullClassName Name of the original class * @return bool true if a cache entry exists */ public function hasCacheEntryForClass(string $fullClassName): bool @@ -191,17 +191,22 @@ public function compile(): int $compiledClasses = []; foreach ($this->objectManager->getRegisteredClassNames() as $fullOriginalClassNames) { foreach ($fullOriginalClassNames as $fullOriginalClassName) { + /** @var class-string $proxyClassName */ + $proxyClassName = str_replace('\\', '_', $fullOriginalClassName); if (isset($this->proxyClasses[$fullOriginalClassName])) { $proxyClassCode = $this->proxyClasses[$fullOriginalClassName]->render(); if ($proxyClassCode !== '') { $class = new ReflectionClass($fullOriginalClassName); $classPathAndFilename = $class->getFileName(); + if ($classPathAndFilename === false) { + throw new \Exception('Missing file name for class', 1744147231); + } $this->cacheOriginalClassFileAndProxyCode($fullOriginalClassName, $classPathAndFilename, $proxyClassCode); - $this->storedProxyClasses[str_replace('\\', '_', $fullOriginalClassName)] = true; + $this->storedProxyClasses[$proxyClassName] = true; $compiledClasses[] = $fullOriginalClassName; } - } elseif ($this->classesCache->has(str_replace('\\', '_', $fullOriginalClassName))) { - $this->storedProxyClasses[str_replace('\\', '_', $fullOriginalClassName)] = true; + } elseif ($this->classesCache->has($proxyClassName)) { + $this->storedProxyClasses[$proxyClassName] = true; } } } @@ -248,6 +253,9 @@ public function getStoredProxyClassMap(): string protected function cacheOriginalClassFileAndProxyCode(string $className, string $pathAndFilename, string $proxyClassCode): void { $classCode = file_get_contents($pathAndFilename); + if ($classCode === false) { + throw new \Exception('Failed to fetch class code ', 1744147125); + } $classCode = $this->replaceClassName($classCode, $pathAndFilename); $classCode = $this->replaceSelfWithStatic($classCode); $classCode = $this->makePrivateConstructorPublic($classCode, $pathAndFilename); @@ -274,13 +282,18 @@ protected function cacheOriginalClassFileAndProxyCode(string $className, string */ protected function stripOpeningPhpTag(string $classCode): string { - return preg_replace('/^\s*\\<\\?php(.*\n|.*)/', '$1', $classCode, 1); + $result = preg_replace('/^\s*\\<\\?php(.*\n|.*)/', '$1', $classCode, 1); + if ($result === null) { + throw new \Exception('Invalid PHP code', 1744147081); + } + + return $result; } /** * Render the source (string) form of a PHP Attribute. - * @param ReflectionAttribute $attribute + * @param ReflectionAttribute $attribute * @return string */ public static function renderAttribute(ReflectionAttribute $attribute): string @@ -343,7 +356,7 @@ public static function renderAnnotation(object $annotation): string /** * Render an array value as string for an annotation. * - * @param array $optionValue + * @param array $optionValue * @return string */ protected static function renderOptionArrayValueAsString(array $optionValue): string @@ -400,6 +413,9 @@ protected function replaceClassName(string $classCode, string $pathAndFilename): return str_replace($classCodeUntilClassName, $classCodeUntilClassNameReplacement, $classCode); } + /** + * @param array $tokens + */ private function getClassNameTokenIndex(array $tokens): ?int { $classToken = null; diff --git a/Neos.Flow/Classes/ObjectManagement/Proxy/ObjectSerializationTrait.php b/Neos.Flow/Classes/ObjectManagement/Proxy/ObjectSerializationTrait.php index 06e5607701..228d6f6890 100644 --- a/Neos.Flow/Classes/ObjectManagement/Proxy/ObjectSerializationTrait.php +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/ObjectSerializationTrait.php @@ -24,6 +24,7 @@ /** * Methods used to serialize objects used by proxy classes. + * @phpstan-ignore trait.unused (probably API?) */ trait ObjectSerializationTrait { diff --git a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php index d414df4d4d..7052cf3aa3 100644 --- a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyClass.php @@ -45,34 +45,38 @@ class ProxyClass protected $fullOriginalClassName; /** - * @var ProxyConstructorGenerator + * @var ?ProxyConstructorGenerator */ protected $constructor; /** - * @var array + * @var array */ protected $methods = []; /** - * @var array + * @var array */ protected $constants = []; /** * Note: Not using ProxyInterface::class here, since the interface names must have a leading backslash. * - * @var array + * @var array */ protected $interfaces = ['\Neos\Flow\ObjectManagement\Proxy\ProxyInterface']; /** - * @var array + * @var array */ protected $traits = []; /** - * @var array + * @var array */ protected $properties = []; @@ -88,10 +92,11 @@ class ProxyClass */ public function __construct(string $fullOriginalClassName) { - if (!str_contains($fullOriginalClassName, '\\')) { + $pivot = strrpos($fullOriginalClassName, '\\'); + if ($pivot === false) { $this->originalClassName = $fullOriginalClassName; } else { - $this->namespace = substr($fullOriginalClassName, 0, strrpos($fullOriginalClassName, '\\')); + $this->namespace = substr($fullOriginalClassName, 0, $pivot); $this->originalClassName = substr($fullOriginalClassName, strlen($this->namespace) + 1); } $this->fullOriginalClassName = $fullOriginalClassName; @@ -186,7 +191,7 @@ public function addProperty(string $name, string $initialValueCode, string $visi * Note that the passed interface names must already have a leading backslash, * for example "\Neos\Flow\Foo\BarInterface". * - * @param array $interfaceNames Fully qualified names of the interfaces to introduce + * @param array $interfaceNames Fully qualified names of the interfaces to introduce * @return void */ public function addInterfaces(array $interfaceNames): void @@ -200,7 +205,7 @@ public function addInterfaces(array $interfaceNames): void * Note that the passed trait names must have a leading backslash, * for example "\Neos\Flow\ObjectManagement\Proxy\PropertyInjectionTrait". * - * @param array $traitNames + * @param array $traitNames * @return void */ public function addTraits(array $traitNames): void @@ -246,7 +251,6 @@ public function render(): string } foreach ($this->methods as $proxyMethod) { - assert($proxyMethod instanceof ProxyMethodGenerator); if ($proxyMethod->willBeRendered()) { $methodsCode .= PHP_EOL . $proxyMethod->generate(); } @@ -274,7 +278,12 @@ protected function buildClassDocumentation(): string { $classReflection = new ClassReflection($this->fullOriginalClassName); - $classDocumentation = str_replace("*/", "* @codeCoverageIgnore\n */", $classReflection->getDocComment()) . "\n"; + $docComment = $classReflection->getDocComment(); + if (!$docComment) { + return ''; + } + + $classDocumentation = str_replace("*/", "* @codeCoverageIgnore\n */", $docComment) . "\n"; foreach ($classReflection->getAttributes() as $attribute) { $classDocumentation .= Compiler::renderAttribute($attribute) . "\n"; } diff --git a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyConstructorGenerator.php b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyConstructorGenerator.php index 4697909129..0963102fdc 100644 --- a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyConstructorGenerator.php +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyConstructorGenerator.php @@ -18,8 +18,11 @@ final class ProxyConstructorGenerator extends ProxyMethodGenerator { private ?string $originalVisibility = null; - /** @phpstan-ignore constructor.unusedParameter */ - public function __construct($name = null, array $parameters = [], $flags = self::FLAG_PUBLIC, $body = null, $docBlock = null) + /** + * @param array $parameters + * @phpstan-ignore constructor.unusedParameter + */ + public function __construct($name = null, array $parameters = [], $flags = self::FLAG_PUBLIC, $body = null, ?DocBlockGenerator $docBlock = null) { if ($docBlock === null) { $docBlock = new DocBlockGenerator(); @@ -46,7 +49,7 @@ public static function fromReflection(\Laminas\Code\Reflection\MethodReflection } $method->setVisibility(self::VISIBILITY_PUBLIC); - if (!empty($reflectionMethod->getDocComment())) { + if ($reflectionMethod->getDocBlock() !== false) { $docBlock = DocBlockGenerator::fromReflection($reflectionMethod->getDocBlock()); $docBlock->setSourceContent(ProxyClassBuilder::AUTOGENERATED_PROXY_METHOD_COMMENT . PHP_EOL . PHP_EOL . $docBlock->getSourceContent()); } else { diff --git a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyMethodGenerator.php b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyMethodGenerator.php index bd501fa412..46d4065c2b 100644 --- a/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyMethodGenerator.php +++ b/Neos.Flow/Classes/ObjectManagement/Proxy/ProxyMethodGenerator.php @@ -44,8 +44,9 @@ public static function copyMethodSignatureAndDocblock(\Laminas\Code\Reflection\M { $instance = parent::copyMethodSignature($reflectionMethod); assert($instance instanceof static); - if ($reflectionMethod->getDocComment() !== false) { - $instance->setDocBlock(DocBlockGenerator::fromReflection($reflectionMethod->getDocBlock())); + $docBlock = $reflectionMethod->getDocBlock(); + if ($docBlock !== false) { + $instance->setDocBlock(DocBlockGenerator::fromReflection($docBlock)); } $instance->fullOriginalClassName = $reflectionMethod->getDeclaringClass()->getName(); $instance->attributesCode = $instance->buildAttributesCode($reflectionMethod); @@ -271,7 +272,7 @@ protected function buildAttributesCode(\ReflectionMethod $reflectionMethod): str /** * Formats the arguments of attributes into a string. * - * @param array $arguments An array of arguments for attributes. + * @param array $arguments An array of arguments for attributes. * @param string $methodName The current method name the proxy code is built for. * @return string The formatted arguments as a string. * @throws UnsupportedAttributeException diff --git a/Neos.Flow/Classes/Package.php b/Neos.Flow/Classes/Package.php index f6ad59cbfb..71f4492289 100644 --- a/Neos.Flow/Classes/Package.php +++ b/Neos.Flow/Classes/Package.php @@ -60,7 +60,6 @@ public function boot(Core\Bootstrap $bootstrap) if ($context->isTesting()) { // TODO: This is technically not necessary as we can register the request handler in the functional bootstrap // A future commit will remove this aftter BuildEssentials is adapted - /** @phpstan-ignore-next-line composer doesnt autoload this class */ $bootstrap->registerRequestHandler(new Tests\FunctionalTestRequestHandler($bootstrap)); } @@ -148,7 +147,6 @@ public function boot(Core\Bootstrap $bootstrap) } }); - /** @phpstan-ignore-next-line composer doesnt autoload this class */ $dispatcher->connect(Tests\FunctionalTestCase::class, 'functionalTestTearDown', Mvc\Routing\RouterCachingService::class, 'flushCaches'); $dispatcher->connect(Configuration\ConfigurationManager::class, 'configurationManagerReady', function (Configuration\ConfigurationManager $configurationManager) { diff --git a/Neos.Flow/Classes/Package/FlowPackageInterface.php b/Neos.Flow/Classes/Package/FlowPackageInterface.php index fe8586530a..ed254c2ebb 100644 --- a/Neos.Flow/Classes/Package/FlowPackageInterface.php +++ b/Neos.Flow/Classes/Package/FlowPackageInterface.php @@ -41,7 +41,7 @@ public function getConfigurationPath(); /** * Returns a generator of filenames of class files provided by functional tests contained in this package * - * @return \Generator + * @return \Generator * @internal */ public function getFunctionalTestsClassFiles(); diff --git a/Neos.Flow/Classes/Package/FlowPackageKey.php b/Neos.Flow/Classes/Package/FlowPackageKey.php index 5e5a72ec6e..fe23b20851 100644 --- a/Neos.Flow/Classes/Package/FlowPackageKey.php +++ b/Neos.Flow/Classes/Package/FlowPackageKey.php @@ -37,7 +37,7 @@ private function __construct( } } - public static function fromString(string $value) + public static function fromString(string $value): self { return new self($value); } @@ -63,6 +63,9 @@ public static function isPackageKeyValid(string $string): bool * case version of the composer name / namespace will be used, with backslashes replaced by dots. * * Else the composer name will be used with the slash replaced by a dot + * + * // @todo replace with ComposerManifest array shape / object from ComposerUtility + * @param array $manifest */ public static function deriveFromManifestOrPath(array $manifest, string $packagePath): self { @@ -77,6 +80,7 @@ public static function deriveFromManifestOrPath(array $manifest, string $package $type = $manifest['type'] ?? null; if (isset($manifest['autoload']['psr-0']) && is_array($manifest['autoload']['psr-0'])) { $namespaces = array_keys($manifest['autoload']['psr-0']); + /** @var string $autoloadNamespace */ $autoloadNamespace = reset($namespaces); } return self::derivePackageKeyInternal($composerName, $type, $packagePath, $autoloadNamespace); @@ -110,7 +114,7 @@ private static function derivePackageKeyInternal(string $composerName, ?string $ } $packageKey = trim($packageKey, '.'); - $packageKey = preg_replace('/[^A-Za-z0-9.]/', '', $packageKey); + $packageKey = preg_replace('/[^A-Za-z0-9.]/', '', $packageKey) ?: ''; return new self($packageKey); } diff --git a/Neos.Flow/Classes/Package/GenericPackage.php b/Neos.Flow/Classes/Package/GenericPackage.php index 20d63c4a5d..5cf3e1a4ce 100644 --- a/Neos.Flow/Classes/Package/GenericPackage.php +++ b/Neos.Flow/Classes/Package/GenericPackage.php @@ -17,6 +17,12 @@ /** * The generic base package that represents third party packages + * + * @phpstan-type AutoloadConfiguration array{ + * namespace: string, + * classPath: string, + * mappingType: string, + * } */ class GenericPackage implements PackageInterface, PackageKeyAwareInterface { @@ -54,12 +60,12 @@ class GenericPackage implements PackageInterface, PackageKeyAwareInterface protected $autoloadTypes; /** - * @var array + * @var array */ protected $autoloadConfiguration; /** - * @var array + * @var array */ protected $flattenedAutoloadConfiguration; @@ -69,7 +75,7 @@ class GenericPackage implements PackageInterface, PackageKeyAwareInterface * @param string $packageKey Key of this package * @param string $composerName * @param string $packagePath Absolute path to the location of the package's composer manifest - * @param array $autoloadConfiguration + * @param array $autoloadConfiguration */ public function __construct($packageKey, $composerName, $packagePath, array $autoloadConfiguration = []) { @@ -82,7 +88,7 @@ public function __construct($packageKey, $composerName, $packagePath, array $aut /** * Returns the array of filenames of the class files * - * @return iterable A Generator for class names (key) and their filename, including the absolute path. + * @return iterable A Generator for class names (key) and their filename, including the absolute path. */ public function getClassFiles() { @@ -121,7 +127,7 @@ public function getComposerName() /** * Returns array of all declared autoload namespaces contained in this package * - * @return array + * @return array * @api */ public function getNamespaces() @@ -157,7 +163,7 @@ public function getPackagePath() } /** - * @return array + * @return array */ public function getAutoloadPaths() { @@ -169,9 +175,9 @@ public function getAutoloadPaths() /** * Get the autoload configuration for this package. Any valid composer "autoload" configuration. * - * @return array + * @return array */ - public function getAutoloadConfiguration() + public function getAutoloadConfiguration(): array { return $this->autoloadConfiguration; } @@ -179,7 +185,7 @@ public function getAutoloadConfiguration() /** * Get a flattened array of autoload configurations that have a predictable pattern (PSR-0, PSR-4) * - * @return array Keys: "namespace", "classPath", "mappingType" + * @return array */ public function getFlattenedAutoloadConfiguration() { diff --git a/Neos.Flow/Classes/Package/Package.php b/Neos.Flow/Classes/Package/Package.php index 45a4b47c30..d026d630f2 100644 --- a/Neos.Flow/Classes/Package/Package.php +++ b/Neos.Flow/Classes/Package/Package.php @@ -34,7 +34,7 @@ public function boot(Bootstrap $bootstrap) /** * Returns a generator of filenames of class files provided by functional tests contained in this package * - * @return \Generator A generator of class names (key) and their filename, including the relative path to the package's directory + * @return \Generator A generator of class names (key) and their filename, including the relative path to the package's directory * @internal */ public function getFunctionalTestsClassFiles() @@ -42,8 +42,12 @@ public function getFunctionalTestsClassFiles() $namespaces = $this->getNamespaces(); if (is_dir($this->packagePath . self::DIRECTORY_TESTS_FUNCTIONAL)) { // TODO REFACTOR replace with usage of "autoload-dev" + $namespace = reset($namespaces); + if (!$namespace) { + throw new \Exception('Missing namespace', 1744144110); + } $namespacePrefix = str_replace('/', '\\', Files::concatenatePaths([ - reset($namespaces), + $namespace, '\\Tests\\Functional\\' ])); foreach ($this->getClassesInNormalizedAutoloadPath($this->packagePath . FlowPackageInterface::DIRECTORY_TESTS_FUNCTIONAL, $namespacePrefix) as $className => $classPath) { diff --git a/Neos.Flow/Classes/Package/PackageFactory.php b/Neos.Flow/Classes/Package/PackageFactory.php index 8f9632e175..d67f720f70 100644 --- a/Neos.Flow/Classes/Package/PackageFactory.php +++ b/Neos.Flow/Classes/Package/PackageFactory.php @@ -27,7 +27,7 @@ class PackageFactory * @param string $packagePath path to package, relative to base path * @param FlowPackageKey $packageKey key / name of the package * @param string $composerName - * @param array $autoloadConfiguration Autoload configuration as defined in composer.json + * @param array $autoloadConfiguration Autoload configuration as defined in composer.json * @param array{className: class-string, pathAndFilename: string}|null $packageClassInformation * @return PackageInterface&PackageKeyAwareInterface * @throws Exception\CorruptPackageException @@ -49,9 +49,6 @@ public function create(string $packagesBasePath, string $packagePath, FlowPackag /** dynamic construction {@see GenericPackage::__construct} */ $package = new $packageClassName($packageKey->value, $composerName, $absolutePackagePath, $autoloadConfiguration); - if (!$package instanceof PackageInterface) { - throw new Exception\CorruptPackageException(sprintf('The package class of package "%s" does not implement \Neos\Flow\Package\PackageInterface. Check the file "%s".', $packageKey->value, $packageClassInformation['pathAndFilename']), 1427193370); - } if (!$package instanceof PackageKeyAwareInterface) { throw new Exception\CorruptPackageException(sprintf('The package class of package "%s" does not implement \Neos\Flow\Package\PackageKeyAwareInterface. Check the file "%s".', $packageKey->value, $packageClassInformation['pathAndFilename']), 1711665156); } @@ -100,8 +97,11 @@ public function detectFlowPackageFilePath(FlowPackageKey $packageKey, $absoluteP $absolutePackageClassPath = Files::concatenatePaths([$absolutePackagePath, $packageClassPathAndFilename]); $packageClassContents = file_get_contents($absolutePackageClassPath); + if (!$packageClassContents) { + throw new \Exception('Could not read package class file', 1744142431); + } $packageClassName = (new PhpAnalyzer($packageClassContents))->extractFullyQualifiedClassName(); - if ($packageClassName === null) { + if ($packageClassName === null || !is_subclass_of($packageClassName, PackageInterface::class)) { throw new Exception\CorruptPackageException(sprintf('The package "%s" does not contain a valid package class. Check if the file "%s" really contains a class.', $packageKey->value, $packageClassPathAndFilename), 1327587091); } diff --git a/Neos.Flow/Classes/Package/PackageInterface.php b/Neos.Flow/Classes/Package/PackageInterface.php index b8ce3e2efd..d08d764258 100644 --- a/Neos.Flow/Classes/Package/PackageInterface.php +++ b/Neos.Flow/Classes/Package/PackageInterface.php @@ -25,7 +25,7 @@ interface PackageInterface /** * Returns the array of filenames of the class files * - * @return iterable An array or yields the class names (key) and their filename, including the relative path to the package's directory + * @return iterable An array or yields the class names (key) and their filename, including the relative path to the package's directory * @api */ public function getClassFiles(); @@ -41,7 +41,7 @@ public function getComposerName(); /** * Returns an array of all namespaces declared for this package. * - * @return array + * @return array * @api */ public function getNamespaces(); diff --git a/Neos.Flow/Classes/Package/PackageManager.php b/Neos.Flow/Classes/Package/PackageManager.php index d2b0522118..fdea0ef576 100644 --- a/Neos.Flow/Classes/Package/PackageManager.php +++ b/Neos.Flow/Classes/Package/PackageManager.php @@ -27,6 +27,13 @@ /** * The default Flow Package Manager * + * @phpstan-type ComposerManifest array{ + * require?: array, + * extra?: array{neos?: array{loading-order?: array{after?: mixed}}}, + * autoload?: array>, + * name: string, + * } + * * @api * @Flow\Scope("singleton") */ @@ -60,7 +67,7 @@ class PackageManager /** * Array of available packages, indexed by package key (case sensitive) * - * @var array + * @var array */ protected $packages = []; @@ -74,7 +81,7 @@ class PackageManager /** * A map between ComposerName and PackageKey, only available when scanAvailablePackages is run * - * @var array + * @var array */ protected $composerNameToPackageKeyMap = []; @@ -93,12 +100,12 @@ class PackageManager /** * Package states configuration as stored in the PackageStates.php file * - * @var array + * @var array */ protected $packageStatesConfiguration = []; /** - * @var array + * @var array */ protected $settings; @@ -111,7 +118,7 @@ class PackageManager * Inject settings into the package manager. Has to be called explicitly on object initialization as * the package manager subpackage is excluded from proxy class building. * - * @param array $settings + * @param array $settings * @return void */ public function injectSettings(array $settings): void @@ -145,8 +152,6 @@ public function initialize(Bootstrap $bootstrap): void $this->bootstrap = $bootstrap; $this->packageStatesConfiguration = $this->getCurrentPackageStates(); $this->registerPackagesFromConfiguration($this->packageStatesConfiguration); - /** @var PackageInterface $package */ - foreach ($this->packages as $package) { if ($package instanceof FlowPackageInterface) { $this->flowPackages[$package->getPackageKey()] = $package; @@ -197,11 +202,11 @@ public function getPackagesBasePath(): string * Returns a PackageInterface object for the specified package. * * @param string $packageKey - * @return PackageInterface The requested package object + * @return PackageInterface&PackageKeyAwareInterface The requested package object * @throws Exception\UnknownPackageException if the specified package is not known * @api */ - public function getPackage($packageKey): PackageInterface + public function getPackage($packageKey): PackageInterface&PackageKeyAwareInterface { if (!$this->isPackageAvailable($packageKey)) { throw new Exception\UnknownPackageException('Package "' . $packageKey . '" is not available. Please check if the package exists and that the package key is correct (package keys are case sensitive).', 1166546734); @@ -214,7 +219,7 @@ public function getPackage($packageKey): PackageInterface * Returns an array of PackageInterface objects of all available packages. * A package is available, if the package directory contains valid meta information. * - * @return array + * @return array * @api */ public function getAvailablePackages(): array @@ -229,7 +234,7 @@ public function getAvailablePackages(): array * @param string $packageState defaults to available * @param string $packageType * - * @return array + * @return array * @throws Exception\InvalidPackageStateException * @api */ @@ -256,9 +261,9 @@ public function getFilteredPackages($packageState = 'available', $packageType = * Returns an array of PackageInterface objects in the given array of packages * that are of the specified package type. * - * @param array $packages Array of PackageInterface objects to be filtered + * @param array $packages Array of PackageInterface objects to be filtered * @param string $packageType Filter out anything that's not of this packageType - * @return array + * @return array */ protected function filterPackagesByType($packages, $packageType): array { @@ -276,7 +281,7 @@ protected function filterPackagesByType($packages, $packageType): array * Create a package, given the package key * * @param string $packageKey The package key of the new package - * @param array $manifest A composer manifest as associative array. + * @param array $manifest A composer manifest as associative array. * @param string $packagesPath If specified, the package will be created in this path, otherwise the default "Application" directory is used * @return PackageInterface The newly created package * @@ -371,7 +376,7 @@ public function createPackage($packageKey, array $manifest = [], $packagesPath = /** * Rescans available packages, order and write a new PackageStates file. * - * @return array The found and sorted package states. + * @return array The found and sorted package states. * @throws Exception * @throws InvalidConfigurationException * @throws FilesException @@ -389,7 +394,7 @@ public function rescanPackages(): array * initialises a package scan if the file was not found or the configuration format * was not current. * - * @return array + * @return array * @throws Exception * @throws InvalidConfigurationException * @throws FilesException @@ -417,7 +422,7 @@ protected function getCurrentPackageStates(): array /** * Load the current package states * - * @return array + * @return array */ protected function loadPackageStates(): array { @@ -428,7 +433,7 @@ protected function loadPackageStates(): array * Scans all directories in the packages directories for available packages. * For each package a Package object is created and stored in $this->packages. * - * @return array + * @return array * @throws Exception * @throws InvalidConfigurationException */ @@ -483,6 +488,8 @@ protected function scanAvailablePackages(): array /** * Traverses directories recursively from the given starting point and yields folder paths, who contain a composer.json. * When a composer.json was found, traversing into lower directories is stopped. + * + * @implements \Generator */ protected function findComposerPackagesInPath(string $startingDirectory): \Generator { @@ -508,6 +515,14 @@ protected function findComposerPackagesInPath(string $startingDirectory): \Gener /** * @throws Exception\CorruptPackageException * @throws Exception\InvalidPackagePathException + * @param ComposerManifest $composerManifest + * @return array{ + * packageKey: string, + * packagePath: string, + * composerName: string, + * autoloadConfiguration: array, + * packageClassInformation: array, + * } */ protected function preparePackageStateConfiguration(FlowPackageKey $packageKey, string $packagePath, array $composerManifest): array { @@ -525,7 +540,7 @@ protected function preparePackageStateConfiguration(FlowPackageKey $packageKey, /** * Requires and registers all packages which were defined in packageStatesConfiguration * - * @param array $packageStatesConfiguration + * @param array $packageStatesConfiguration * @throws Exception\CorruptPackageException */ protected function registerPackagesFromConfiguration($packageStatesConfiguration): void @@ -541,7 +556,7 @@ protected function registerPackagesFromConfiguration($packageStatesConfiguration * to all relevant data arrays. * * @param string $composerName - * @param array $packageStateConfiguration + * @param array $packageStateConfiguration * @return void * @throws Exception\CorruptPackageException */ @@ -558,8 +573,8 @@ protected function registerPackageFromStateConfiguration($composerName, $package * Takes the given packageStatesConfiguration, sorts it by dependencies, saves it and returns * the ordered list * - * @param array $packageStates - * @return array + * @param array $packageStates + * @return array * @throws Exception\PackageStatesFileNotWritableException * @throws FilesException */ @@ -574,7 +589,7 @@ protected function sortAndSavePackageStates(array $packageStates): array /** * Save the given (ordered) array of package states data * - * @param array $orderedPackageStates + * @param array $orderedPackageStates * @throws Exception\PackageStatesFileNotWritableException * @throws FilesException */ @@ -620,12 +635,15 @@ protected function savePackageStates(array $orderedPackageStates): void * and package configurations arrays holds all packages in the correct * initialization order. * - * @param array $packageStates The unordered package states - * @return array ordered package states. + * @param array $packageStates The unordered package states + * @return array ordered package states. */ protected function sortAvailablePackagesByDependencies(array $packageStates): array { - $packageOrderResolver = new PackageOrderResolver($packageStates['packages'], $this->collectPackageManifestData($packageStates)); + $packageOrderResolver = new PackageOrderResolver( + $packageStates['packages'], + $this->collectPackageManifestData($packageStates) + ); $packageStates['packages'] = $packageOrderResolver->sort(); return $packageStates; @@ -634,8 +652,8 @@ protected function sortAvailablePackagesByDependencies(array $packageStates): ar /** * Collects the manifest data for all packages in the given package states array * - * @param array $packageStates - * @return array + * @param array $packageStates + * @return array */ protected function collectPackageManifestData(array $packageStates): array { diff --git a/Neos.Flow/Classes/Package/PackageOrderResolver.php b/Neos.Flow/Classes/Package/PackageOrderResolver.php index 8fae9f6b99..c1f9235be8 100644 --- a/Neos.Flow/Classes/Package/PackageOrderResolver.php +++ b/Neos.Flow/Classes/Package/PackageOrderResolver.php @@ -3,21 +3,23 @@ /** * A simple package dependency order solver. Just sorts by simple dependencies, does no checking or versions. + * + * @phpstan-import-type ComposerManifest from PackageManager */ class PackageOrderResolver { /** - * @var array + * @var array */ protected $manifestData; /** - * @var array + * @var array> */ protected $packageStates; /** - * @var array + * @var array */ protected $sortedPackages; @@ -25,13 +27,13 @@ class PackageOrderResolver * Array with state information of still unsorted packages. * The key is a package key, the value is "-1" if it is on stack for cycle detection; otherwise it is the number of times it was attempted to sort it already. * - * @var array + * @var array */ protected $unsortedPackages; /** - * @param array $packages The array of package states to order by dependencies - * @param array $manifestData The manifests of all given packages for reading dependencies + * @param array> $packages The array of package states to order by dependencies + * @param array $manifestData The manifests of all given packages for reading dependencies */ public function __construct(array $packages, array $manifestData) { @@ -43,7 +45,7 @@ public function __construct(array $packages, array $manifestData) /** * Sorts the packages and returns the sorted packages array * - * @return array + * @return array */ public function sort() { @@ -100,7 +102,6 @@ protected function sortPackage($packageKey) $unresolvedDependencies += $this->sortListBefore($packageKey, $sortingConfiguration); } - /** @var array $packageState */ $packageState = $this->packageStates[$packageKey]; $this->unsortedPackages[$packageKey] = $iterationForPackage + 1; if ($unresolvedDependencies === 0) { diff --git a/Neos.Flow/Classes/Persistence/AbstractPersistenceManager.php b/Neos.Flow/Classes/Persistence/AbstractPersistenceManager.php index 2db99302da..c887f1fc75 100644 --- a/Neos.Flow/Classes/Persistence/AbstractPersistenceManager.php +++ b/Neos.Flow/Classes/Persistence/AbstractPersistenceManager.php @@ -22,12 +22,12 @@ abstract class AbstractPersistenceManager implements PersistenceManagerInterface { /** - * @var array + * @var array */ protected $settings = []; /** - * @var array + * @var array */ protected $newObjects = []; @@ -46,7 +46,7 @@ abstract class AbstractPersistenceManager implements PersistenceManagerInterface * Injects the Flow settings, the persistence part is kept * for further use. * - * @param array $settings + * @param array> $settings * @return void */ public function injectSettings(array $settings): void diff --git a/Neos.Flow/Classes/Persistence/AllowedObjectsContainer.php b/Neos.Flow/Classes/Persistence/AllowedObjectsContainer.php index a013cec08e..c877ce1960 100644 --- a/Neos.Flow/Classes/Persistence/AllowedObjectsContainer.php +++ b/Neos.Flow/Classes/Persistence/AllowedObjectsContainer.php @@ -18,6 +18,7 @@ /** * A container for the list of allowed objects to be persisted during this request. * + * @extends \SplObjectStorage * @Flow\Scope("singleton") */ final class AllowedObjectsContainer extends \SplObjectStorage diff --git a/Neos.Flow/Classes/Persistence/Aspect/EmbeddedValueObjectPointcutFilter.php b/Neos.Flow/Classes/Persistence/Aspect/EmbeddedValueObjectPointcutFilter.php index e4526c1003..359e31a0a2 100644 --- a/Neos.Flow/Classes/Persistence/Aspect/EmbeddedValueObjectPointcutFilter.php +++ b/Neos.Flow/Classes/Persistence/Aspect/EmbeddedValueObjectPointcutFilter.php @@ -39,7 +39,7 @@ public function injectReflectionService(\Neos\Flow\Reflection\ReflectionService /** * Checks if the specified class and method matches against the filter * - * @param string $className Name of the class to check against + * @param class-string $className Name of the class to check against * @param string $methodName Name of the method to check against * @param string $methodDeclaringClassName Name of the class the method was originally declared in * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection. @@ -64,7 +64,7 @@ public function hasRuntimeEvaluationsDefinition() /** * Returns runtime evaluations for a previously matched pointcut * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition() { diff --git a/Neos.Flow/Classes/Persistence/Aspect/PersistenceMagicAspect.php b/Neos.Flow/Classes/Persistence/Aspect/PersistenceMagicAspect.php index 7daf1e95dc..46b8d4e506 100644 --- a/Neos.Flow/Classes/Persistence/Aspect/PersistenceMagicAspect.php +++ b/Neos.Flow/Classes/Persistence/Aspect/PersistenceMagicAspect.php @@ -49,21 +49,21 @@ class PersistenceMagicAspect /** * @Flow\Pointcut("classAnnotatedWith(Neos\Flow\Annotations\Entity) || classAnnotatedWith(Doctrine\ORM\Mapping\Entity)") */ - public function isEntity() + public function isEntity(): void { } /** * @Flow\Pointcut("classAnnotatedWith(Neos\Flow\Annotations\ValueObject) && !filter(Neos\Flow\Persistence\Aspect\EmbeddedValueObjectPointcutFilter)") */ - public function isNonEmbeddedValueObject() + public function isNonEmbeddedValueObject(): void { } /** * @Flow\Pointcut("Neos\Flow\Persistence\Aspect\PersistenceMagicAspect->isEntity || Neos\Flow\Persistence\Aspect\PersistenceMagicAspect->isNonEmbeddedValueObject") */ - public function isEntityOrValueObject() + public function isEntityOrValueObject(): void { } @@ -114,6 +114,9 @@ public function generateValueHash(JoinPointInterface $joinPoint) $hashSourceParts = []; $classSchema = $this->reflectionService->getClassSchema($proxyClassName); + if (!$classSchema) { + throw new \Exception('Missing class schema for proxy class ' . $proxyClassName); + } foreach ($classSchema->getProperties() as $property => $propertySchema) { // Currently, private properties are transient. Should this behaviour change, they need to be included // in the value hash generation @@ -138,6 +141,6 @@ public function generateValueHash(JoinPointInterface $joinPoint) $serializedSource = ($this->useIgBinary === true) ? igbinary_serialize($hashSourceParts) : serialize($hashSourceParts); $proxy = $joinPoint->getProxy(); - ObjectAccess::setProperty($proxy, 'Persistence_Object_Identifier', sha1($serializedSource), true); + ObjectAccess::setProperty($proxy, 'Persistence_Object_Identifier', sha1($serializedSource ?: ''), true); } } diff --git a/Neos.Flow/Classes/Persistence/Doctrine/ArrayTypeConverter.php b/Neos.Flow/Classes/Persistence/Doctrine/ArrayTypeConverter.php index 46bdd4ae3c..0a2de45843 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/ArrayTypeConverter.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/ArrayTypeConverter.php @@ -63,16 +63,15 @@ class ArrayTypeConverter extends AbstractTypeConverter * * @param mixed $source * @param string $targetType - * @param array $convertedChildProperties - * @param PropertyMappingConfigurationInterface $configuration - * @return mixed|Error the target type, or an error object if a user-error occurred + * @param array $convertedChildProperties + * @return array the target type, or an error object if a user-error occurred * @throws TypeConverterException thrown in case a developer error occurred * @api */ public function convertFrom($source, $targetType, array $convertedChildProperties = [], ?PropertyMappingConfigurationInterface $configuration = null) { $result = []; - $convertElements = $configuration->getConfigurationValue(ArrayTypeConverter::class, self::CONFIGURATION_CONVERT_ELEMENTS); + $convertElements = $configuration?->getConfigurationValue(ArrayTypeConverter::class, self::CONFIGURATION_CONVERT_ELEMENTS) ?: true; foreach ($source as $element) { if ($convertElements === true) { $element = $this->propertyMapper->convert($element, 'array', $configuration); diff --git a/Neos.Flow/Classes/Persistence/Doctrine/CountWalker.php b/Neos.Flow/Classes/Persistence/Doctrine/CountWalker.php index c1695d1b3a..f98cfeb319 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/CountWalker.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/CountWalker.php @@ -34,7 +34,7 @@ public function walkSelectStatement(SelectStatement $AST) $parent = null; $parentName = null; foreach ($this->getQueryComponents() as $dqlAlias => $qComp) { - if ($qComp['parent'] === null && $qComp['nestingLevel'] === 0) { + if (($qComp['parent'] ?? null) === null && $qComp['nestingLevel'] === 0) { $parent = $qComp; $parentName = $dqlAlias; break; @@ -45,10 +45,13 @@ public function walkSelectStatement(SelectStatement $AST) $AST->selectClause->isDistinct = true; } + if (!is_string($parentName)) { + throw new \Exception('Missing parent name', 1744138349); + } $pathExpression = new PathExpression( PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $parentName, - $parent['metadata']->getSingleIdentifierFieldName() + ($parent['metadata'] ?? null)?->getSingleIdentifierFieldName() ); $pathExpression->type = PathExpression::TYPE_STATE_FIELD; diff --git a/Neos.Flow/Classes/Persistence/Doctrine/DataTypes/JsonArrayType.php b/Neos.Flow/Classes/Persistence/Doctrine/DataTypes/JsonArrayType.php index 33f829684a..0be649ad1f 100755 --- a/Neos.Flow/Classes/Persistence/Doctrine/DataTypes/JsonArrayType.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/DataTypes/JsonArrayType.php @@ -60,7 +60,7 @@ public function getMappedDatabaseTypes(AbstractPlatform $platform): array * * @param mixed $value The value to convert. * @param AbstractPlatform $platform The currently used database platform. - * @return array|null The PHP representation of the value. + * @return array|null The PHP representation of the value. * @throws ConversionException * @throws TypeConverterException */ @@ -114,6 +114,9 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?str */ protected function initializeDependencies(): void { + if (!Bootstrap::$staticObjectManager) { + throw new \Exception('Cannot initialize dependencies without the static object manager', 1744398442); + } if ($this->persistenceManager === null) { $this->persistenceManager = Bootstrap::$staticObjectManager->get(PersistenceManagerInterface::class); $this->reflectionService = Bootstrap::$staticObjectManager->get(ReflectionService::class); @@ -124,11 +127,15 @@ protected function initializeDependencies(): void * Traverses the $array and replaces known persisted objects (tuples of * type and identifier) with actual instances. * + * @param array $array * @throws TypeConverterException */ protected function decodeObjectReferences(array &$array): void { - assert($this->persistenceManager instanceof PersistenceManagerInterface); + $persistenceManager = $this->persistenceManager; + if (!$persistenceManager) { + throw new \Exception('PersistenceManager is missing', 1744138182); + } foreach ($array as &$value) { if (!is_array($value)) { @@ -138,7 +145,7 @@ protected function decodeObjectReferences(array &$array): void if (isset($value['__value_object_value'], $value['__value_object_type'])) { $value = self::deserializeValueObject($value); } elseif (isset($value['__flow_object_type'])) { - $value = $this->persistenceManager->getObjectByIdentifier($value['__identifier'], $value['__flow_object_type'], true); + $value = $persistenceManager->getObjectByIdentifier($value['__identifier'], $value['__flow_object_type'], true); } else { $this->decodeObjectReferences($value); } @@ -146,6 +153,7 @@ protected function decodeObjectReferences(array &$array): void } /** + * @param array $serializedValueObject * @throws \InvalidArgumentException * @throws TypeConverterException */ @@ -168,12 +176,16 @@ public static function deserializeValueObject(array $serializedValueObject): \Js * Traverses the $array and replaces known persisted objects with a tuple of * type and identifier. * + * @param array $array * @throws \RuntimeException * @throws \JsonException */ protected function encodeObjectReferences(array &$array): void { - assert($this->persistenceManager instanceof PersistenceManagerInterface); + $persistenceManager = $this->persistenceManager; + if (!$persistenceManager) { + throw new \Exception('PersistenceManager is missing', 1744138137); + } foreach ($array as &$value) { if (is_array($value)) { @@ -197,7 +209,7 @@ protected function encodeObjectReferences(array &$array): void throw new \RuntimeException('Collection in array properties is not supported', 1375196581); } elseif ($value instanceof \ArrayObject) { throw new \RuntimeException('ArrayObject in array properties is not supported', 1375196582); - } elseif ($this->persistenceManager->isNewObject($value) === false + } elseif ($persistenceManager->isNewObject($value) === false && ( $this->reflectionService->isClassAnnotatedWith($propertyClassName, Flow\Entity::class) || $this->reflectionService->isClassAnnotatedWith($propertyClassName, Flow\ValueObject::class) @@ -206,7 +218,7 @@ protected function encodeObjectReferences(array &$array): void ) { $value = [ '__flow_object_type' => $propertyClassName, - '__identifier' => $this->persistenceManager->getIdentifierByObject($value) + '__identifier' => $persistenceManager->getIdentifierByObject($value) ]; } elseif ($value instanceof \JsonSerializable && DenormalizingObjectConverter::isDenormalizable(get_class($value)) @@ -219,6 +231,7 @@ protected function encodeObjectReferences(array &$array): void /** * @throws \RuntimeException * @throws \JsonException + * @return array */ public static function serializeValueObject(\JsonSerializable $valueObject): array { diff --git a/Neos.Flow/Classes/Persistence/Doctrine/EntityManagerConfiguration.php b/Neos.Flow/Classes/Persistence/Doctrine/EntityManagerConfiguration.php index 93e19e6942..3225e80660 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/EntityManagerConfiguration.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/EntityManagerConfiguration.php @@ -99,7 +99,7 @@ public function configureEntityManager(Connection $connection, Configuration $co } /** - * @param array $configuredSubscribers + * @param array $configuredSubscribers * @param EventManager $eventManager * @throws IllegalObjectTypeException */ @@ -115,7 +115,7 @@ protected function registerEventSubscribers(array $configuredSubscribers, EventM } /** - * @param array $configuredListeners + * @param array, events: string|array}> $configuredListeners * @param EventManager $eventManager */ protected function registerEventListeners(array $configuredListeners, EventManager $eventManager): void @@ -130,7 +130,7 @@ protected function registerEventListeners(array $configuredListeners, EventManag * Apply configured settings regarding DQL to the Doctrine Configuration. * At the moment, these are custom DQL functions. * - * @param array $configuredSettings + * @param array $configuredSettings * @param Configuration $doctrineConfiguration * @return void */ @@ -174,7 +174,7 @@ protected function applyCacheConfiguration(Configuration $config): void /** * Apply configured settings regarding Doctrine's second level cache. * - * @param array $configuredSettings + * @param array $configuredSettings * @param Configuration $doctrineConfiguration * @return void * @throws NoSuchCacheException diff --git a/Neos.Flow/Classes/Persistence/Doctrine/EntityManagerFactory.php b/Neos.Flow/Classes/Persistence/Doctrine/EntityManagerFactory.php index 6380139d5e..3fc9c34544 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/EntityManagerFactory.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/EntityManagerFactory.php @@ -55,7 +55,7 @@ class EntityManagerFactory protected $environment; /** - * @var array + * @var array */ protected $settings = []; @@ -63,7 +63,7 @@ class EntityManagerFactory * Injects the Flow settings, the persistence part is kept * for further use. * - * @param array $settings + * @param array> $settings * @return void * @throws InvalidConfigurationException */ @@ -138,7 +138,7 @@ public function create() * @param EventManager $eventManager * @Flow\Signal */ - public function emitBeforeDoctrineEntityManagerCreation(Connection $connection, Configuration $config, EventManager $eventManager) + public function emitBeforeDoctrineEntityManagerCreation(Connection $connection, Configuration $config, EventManager $eventManager): void { } @@ -147,7 +147,7 @@ public function emitBeforeDoctrineEntityManagerCreation(Connection $connection, * @param EntityManager $entityManager * @Flow\Signal */ - public function emitAfterDoctrineEntityManagerCreation(Configuration $config, EntityManager $entityManager) + public function emitAfterDoctrineEntityManagerCreation(Configuration $config, EntityManager $entityManager): void { } diff --git a/Neos.Flow/Classes/Persistence/Doctrine/Mapping/ClassMetadata.php b/Neos.Flow/Classes/Persistence/Doctrine/Mapping/ClassMetadata.php index 4a9fc7c8dc..5e24fd0d14 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/Mapping/ClassMetadata.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/Mapping/ClassMetadata.php @@ -17,6 +17,8 @@ /** * A ClassMetadata instance holds all the object-relational mapping metadata * of an entity and it's associations. + * + * @extends \Doctrine\ORM\Mapping\ClassMetadata */ class ClassMetadata extends \Doctrine\ORM\Mapping\ClassMetadata { diff --git a/Neos.Flow/Classes/Persistence/Doctrine/Mapping/ClassMetadataFactory.php b/Neos.Flow/Classes/Persistence/Doctrine/Mapping/ClassMetadataFactory.php index eecf79fab8..ee630624e4 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/Mapping/ClassMetadataFactory.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/Mapping/ClassMetadataFactory.php @@ -21,7 +21,7 @@ class ClassMetadataFactory extends \Doctrine\ORM\Mapping\ClassMetadataFactory /** * Creates a new ClassMetadata instance for the given class name. * - * @param string $className + * @param class-string $className * @return ClassMetadata */ protected function newClassMetadataInstance($className) diff --git a/Neos.Flow/Classes/Persistence/Doctrine/Mapping/Driver/FlowAnnotationDriver.php b/Neos.Flow/Classes/Persistence/Doctrine/Mapping/Driver/FlowAnnotationDriver.php index 8539422da6..d9fe31846a 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/Mapping/Driver/FlowAnnotationDriver.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/Mapping/Driver/FlowAnnotationDriver.php @@ -70,7 +70,7 @@ class FlowAnnotationDriver implements DoctrineMappingDriverInterface, PointcutFi protected $entityManager; /** - * @var array + * @var ?array */ protected $classNames; @@ -109,7 +109,7 @@ public function setEntityManager(EntityManagerInterface $entityManager) /** * Fetch a class schema for the given class, if possible. * - * @param string $className + * @param class-string $className * @return ClassSchema * @throws ClassSchemaNotFoundException */ @@ -126,7 +126,7 @@ protected function getClassSchema($className) /** * Check for $className being an aggregate root. * - * @param string $className + * @param class-string $className * @param string $propertySourceHint * @return boolean * @throws ClassSchemaNotFoundException @@ -146,7 +146,7 @@ protected function isAggregateRoot($className, $propertySourceHint) /** * Check for $className being a value object. * - * @param string $className + * @param class-string $className * @param string $propertySourceHint * @return boolean * @throws ClassSchemaNotFoundException @@ -166,8 +166,8 @@ protected function isValueObject($className, $propertySourceHint) /** * Loads the metadata for the specified class into the provided container. * - * @param string $className - * @param ClassMetadata $metadata + * @param class-string $className + * @param ClassMetadata $metadata * @return void * @throws ORM\MappingException * @throws \UnexpectedValueException @@ -178,9 +178,8 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) * This is the actual type we have at this point, but we cannot change the * signature due to inheritance. * - * @var ORM\ClassMetadata $metadata + * @var ORM\ClassMetadata $metadata */ - try { $class = $metadata->getReflectionClass(); $classSchema = $this->getClassSchema($class->getName()); @@ -191,24 +190,28 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) // Evaluate Entity annotation if (isset($classAnnotations[ORM\MappedSuperclass::class])) { + /** @var ORM\MappedSuperclass $mappedSuperclassAnnotation */ $mappedSuperclassAnnotation = $classAnnotations[ORM\MappedSuperclass::class]; if ($mappedSuperclassAnnotation->repositoryClass !== null) { $metadata->setCustomRepositoryClass($mappedSuperclassAnnotation->repositoryClass); } $metadata->isMappedSuperclass = true; } elseif (isset($classAnnotations[Flow\Entity::class]) || isset($classAnnotations[ORM\Entity::class])) { - $entityAnnotation = isset($classAnnotations[Flow\Entity::class]) ? $classAnnotations[Flow\Entity::class] : $classAnnotations[ORM\Entity::class]; - if ($entityAnnotation->repositoryClass !== null) { + /** @var Flow\Entity|ORM\Entity $entityAnnotation */ + $entityAnnotation = $classAnnotations[Flow\Entity::class] ?? $classAnnotations[ORM\Entity::class]; + if ($entityAnnotation->repositoryClass !== null && is_subclass_of($entityAnnotation->repositoryClass, EntityRepository::class)) { $metadata->setCustomRepositoryClass($entityAnnotation->repositoryClass); } elseif ($classSchema->getRepositoryClassName() !== null) { - if ($this->reflectionService->isClassImplementationOf($classSchema->getRepositoryClassName(), EntityRepository::class)) { - $metadata->setCustomRepositoryClass($classSchema->getRepositoryClassName()); + $repositoryClassName = $classSchema->getRepositoryClassName(); + if (is_subclass_of($repositoryClassName, EntityRepository::class)) { + $metadata->setCustomRepositoryClass($repositoryClassName); } } if ($entityAnnotation->readOnly) { $metadata->markReadOnly(); } } elseif (isset($classAnnotations[Flow\ValueObject::class])) { + /** @var Flow\ValueObject $valueObjectAnnotation */ $valueObjectAnnotation = $classAnnotations[Flow\ValueObject::class]; if ($valueObjectAnnotation->embedded === true) { $metadata->isEmbeddedClass = true; @@ -225,6 +228,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) // Evaluate Table annotation $primaryTable = []; if (isset($classAnnotations[ORM\Table::class])) { + /** @var ORM\Table $tableAnnotation */ $tableAnnotation = $classAnnotations[ORM\Table::class]; $primaryTable = [ 'name' => $tableAnnotation->name, @@ -276,6 +280,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) // Evaluate @Cache annotation if (isset($classAnnotations[ORM\Cache::class])) { + /** @var ORM\Cache $cacheAnnotation */ $cacheAnnotation = $classAnnotations[ORM\Cache::class]; $cacheMap = [ 'region' => $cacheAnnotation->region, @@ -287,6 +292,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) // Evaluate NamedNativeQueries annotation if (isset($classAnnotations[ORM\NamedNativeQueries::class])) { + /** @var ORM\NamedNativeQueries $namedNativeQueriesAnnotation */ $namedNativeQueriesAnnotation = $classAnnotations[ORM\NamedNativeQueries::class]; foreach ($namedNativeQueriesAnnotation->value as $namedNativeQuery) { @@ -301,6 +307,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) // Evaluate SqlResultSetMappings annotation if (isset($classAnnotations[ORM\SqlResultSetMappings::class])) { + /** @var ORM\SqlResultSetMappings $sqlResultSetMappingsAnnotation */ $sqlResultSetMappingsAnnotation = $classAnnotations[ORM\SqlResultSetMappings::class]; foreach ($sqlResultSetMappingsAnnotation->value as $resultSetMapping) { @@ -339,16 +346,10 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) // Evaluate NamedQueries annotation if (isset($classAnnotations[ORM\NamedQueries::class])) { + /** @var ORM\NamedQueries $namedQueriesAnnotation */ $namedQueriesAnnotation = $classAnnotations[ORM\NamedQueries::class]; - if (!is_array($namedQueriesAnnotation->value)) { - throw new \UnexpectedValueException('@ORM\NamedQueries should contain an array of @ORM\NamedQuery annotations.'); - } - foreach ($namedQueriesAnnotation->value as $namedQuery) { - if (!($namedQuery instanceof ORM\NamedQuery)) { - throw new \UnexpectedValueException('@ORM\NamedQueries should contain an array of @ORM\NamedQuery annotations.'); - } $metadata->addNamedQuery([ 'name' => $namedQuery->name, 'query' => $namedQuery->query @@ -358,17 +359,19 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) // Evaluate InheritanceType annotation if (isset($classAnnotations[ORM\InheritanceType::class])) { + /** @var ORM\InheritanceType $inheritanceTypeAnnotation */ $inheritanceTypeAnnotation = $classAnnotations[ORM\InheritanceType::class]; $inheritanceType = constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . strtoupper($inheritanceTypeAnnotation->value)); if ($inheritanceType !== ORM\ClassMetadata::INHERITANCE_TYPE_NONE) { // Evaluate DiscriminatorColumn annotation if (isset($classAnnotations[ORM\DiscriminatorColumn::class])) { + /** @var ORM\DiscriminatorColumn $discriminatorColumnAnnotation */ $discriminatorColumnAnnotation = $classAnnotations[ORM\DiscriminatorColumn::class]; $discriminatorColumn = [ 'name' => $discriminatorColumnAnnotation->name, - 'type' => $discriminatorColumnAnnotation->type, - 'length' => $discriminatorColumnAnnotation->length, + 'type' => $discriminatorColumnAnnotation->type ?: throw new \Exception('Cannot use untyped discriminator column', 1744137386), + 'length' => $discriminatorColumnAnnotation->length ?: throw new \Exception('Cannot use discriminator column without length', 1744137404), 'columnDefinition' => $discriminatorColumnAnnotation->columnDefinition ]; } else { @@ -379,6 +382,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) // Evaluate DiscriminatorMap annotation if (isset($classAnnotations[ORM\DiscriminatorMap::class])) { + /** @var ORM\DiscriminatorMap $discriminatorMapAnnotation */ $discriminatorMapAnnotation = $classAnnotations[ORM\DiscriminatorMap::class]; $discriminatorMap = $discriminatorMapAnnotation->value; } else { @@ -407,8 +411,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) // Evaluate DoctrineChangeTrackingPolicy annotation if (isset($classAnnotations[ORM\ChangeTrackingPolicy::class])) { + /** @var ORM\ChangeTrackingPolicy $changeTrackingAnnotation */ $changeTrackingAnnotation = $classAnnotations[ORM\ChangeTrackingPolicy::class]; - $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' . strtoupper($changeTrackingAnnotation->value))); + $metadata->setChangeTrackingPolicy(constant( + 'Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' . strtoupper($changeTrackingAnnotation->value) + )); } else { $metadata->setChangeTrackingPolicy(ORM\ClassMetadata::CHANGETRACKING_DEFERRED_EXPLICIT); } @@ -425,7 +432,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) && !isset($primaryTable['uniqueConstraints'])) { $idProperties = array_keys($classSchema->getIdentityProperties()); if (array_diff($idProperties, $metadata->getIdentifierFieldNames()) !== []) { - $uniqueIndexName = $this->truncateIdentifier('flow_identity_' . $primaryTable['name']); + $primaryTableName = $primaryTable['name'] ?? null; + if (!$primaryTableName) { + throw new \Exception('Missing primary table name', 1744135393); + } + $uniqueIndexName = $this->truncateIdentifier('flow_identity_' . $primaryTableName); foreach ($idProperties as $idProperty) { $primaryTable['uniqueConstraints'][$uniqueIndexName]['columns'][] = $metadata->fieldMappings[$idProperty]['columnName'] ?? strtolower($idProperty); } @@ -534,11 +545,11 @@ protected function buildJoinTableColumnName($className) * Check if the referenced column name is set (and valid) and if not make sure * it is initialized properly. * - * @param array $joinColumns - * @param array $mapping + * @param array $joinColumns + * @param array $mapping * @param \ReflectionProperty $property * @param integer $direction regular or inverse mapping (use is to be coded) - * @return array + * @return array */ protected function buildJoinColumnsIfNeeded(array $joinColumns, array $mapping, \ReflectionProperty $property, $direction = self::MAPPING_REGULAR) { @@ -574,7 +585,7 @@ protected function buildJoinColumnsIfNeeded(array $joinColumns, array $mapping, /** * Evaluate the property annotations and amend the metadata accordingly. * - * @param ORM\ClassMetadataInfo $metadata + * @param ORM\ClassMetadataInfo $metadata * @return void * @throws ORM\MappingException */ @@ -601,7 +612,7 @@ protected function evaluatePropertyAnnotations(ORM\ClassMetadataInfo $metadata) $mapping['fieldName'] = $property->getName(); $mapping['columnName'] = strtolower($property->getName()); $mapping['targetEntity'] = $propertyMetaData['type']; - $mapping['nullable'] = $propertyMetaData['nullable'] ?? false; + $mapping['nullable'] = $propertyMetaData['nullable']; $joinColumns = $this->evaluateJoinColumnAnnotations($property); @@ -621,13 +632,17 @@ protected function evaluatePropertyAnnotations(ORM\ClassMetadataInfo $metadata) $mapping['inversedBy'] = $oneToOneAnnotation->inversedBy; if ($oneToOneAnnotation->cascade !== null) { $mapping['cascade'] = $oneToOneAnnotation->cascade; + /** @phpstan-ignore argument.type (this really is a class-string -.-) */ } elseif ($this->isValueObject($mapping['targetEntity'], $className)) { $mapping['cascade'] = ['persist']; + /** @phpstan-ignore argument.type (this really is a class-string -.-) */ } elseif ($this->isAggregateRoot($mapping['targetEntity'], $className) === false) { $mapping['cascade'] = ['all']; } // We need to apply our value for non-aggregate roots first, because Doctrine sets a default value for orphanRemoval (see #1127) + /** @phpstan-ignore argument.type (this really is a class-string -.-) */ if ($this->isAggregateRoot($mapping['targetEntity'], $className) === false && + /** @phpstan-ignore argument.type (this really is a class-string -.-) */ $this->isValueObject($mapping['targetEntity'], $className) === false) { $mapping['orphanRemoval'] = true; } elseif ($oneToOneAnnotation->orphanRemoval !== null) { @@ -644,14 +659,18 @@ protected function evaluatePropertyAnnotations(ORM\ClassMetadataInfo $metadata) } if ($oneToManyAnnotation->cascade !== null) { $mapping['cascade'] = $oneToManyAnnotation->cascade; + /** @phpstan-ignore argument.type (this really is a class-string -.-) */ } elseif ($this->isValueObject($mapping['targetEntity'], $className)) { $mapping['cascade'] = ['persist']; + /** @phpstan-ignore argument.type (this really is a class-string -.-) */ } elseif ($this->isAggregateRoot($mapping['targetEntity'], $className) === false) { $mapping['cascade'] = ['all']; } $mapping['indexBy'] = $oneToManyAnnotation->indexBy; // We need to apply our value for non-aggregate roots first, because Doctrine sets a default value for orphanRemoval (see #1127) + /** @phpstan-ignore argument.type (this really is a class-string -.-) */ if ($this->isAggregateRoot($mapping['targetEntity'], $className) === false && + /** @phpstan-ignore argument.type (this really is a class-string -.-) */ $this->isValueObject($mapping['targetEntity'], $className) === false) { $mapping['orphanRemoval'] = true; } elseif ($oneToManyAnnotation->orphanRemoval !== null) { @@ -712,8 +731,10 @@ protected function evaluatePropertyAnnotations(ORM\ClassMetadataInfo $metadata) $mapping['joinColumns'] = $this->buildJoinColumnsIfNeeded($joinColumns, $mapping, $property); if ($manyToOneAnnotation->cascade !== null) { $mapping['cascade'] = $manyToOneAnnotation->cascade; + /** @phpstan-ignore argument.type (this really is a class-string -.-) */ } elseif ($this->isValueObject($mapping['targetEntity'], $className)) { $mapping['cascade'] = ['persist']; + /** @phpstan-ignore argument.type (this really is a class-string -.-) */ } elseif ($this->isAggregateRoot($mapping['targetEntity'], $className) === false) { $mapping['cascade'] = ['all']; } @@ -749,8 +770,10 @@ protected function evaluatePropertyAnnotations(ORM\ClassMetadataInfo $metadata) $mapping['inversedBy'] = $manyToManyAnnotation->inversedBy; if ($manyToManyAnnotation->cascade !== null) { $mapping['cascade'] = $manyToManyAnnotation->cascade; + /** @phpstan-ignore argument.type (this really is a class-string -.-) */ } elseif ($this->isValueObject($mapping['targetEntity'], $className)) { $mapping['cascade'] = ['persist']; + /** @phpstan-ignore argument.type (this really is a class-string -.-) */ } elseif ($this->isAggregateRoot($mapping['targetEntity'], $className) === false) { $mapping['cascade'] = ['all']; } @@ -798,7 +821,7 @@ protected function evaluatePropertyAnnotations(ORM\ClassMetadataInfo $metadata) break; default: if (strpos($propertyMetaData['type'], '\\') !== false) { - if ($this->reflectionService->isClassAnnotatedWith($propertyMetaData['type'], Flow\ValueObject::class)) { + if (class_exists($propertyMetaData['type']) && $this->reflectionService->isClassAnnotatedWith($propertyMetaData['type'], Flow\ValueObject::class)) { $valueObjectAnnotation = $this->reflectionService->getClassAnnotation($propertyMetaData['type'], Flow\ValueObject::class); if ($valueObjectAnnotation && $valueObjectAnnotation->embedded === true) { $mapping['class'] = $propertyMetaData['type']; @@ -833,11 +856,11 @@ protected function evaluatePropertyAnnotations(ORM\ClassMetadataInfo $metadata) // Check for SequenceGenerator/TableGenerator definition if ($seqGeneratorAnnotation = $this->reader->getPropertyAnnotation($property, ORM\SequenceGenerator::class)) { - $metadata->setSequenceGeneratorDefinition([ + $metadata->setSequenceGeneratorDefinition(array_filter([ 'sequenceName' => $seqGeneratorAnnotation->sequenceName, 'allocationSize' => $seqGeneratorAnnotation->allocationSize, 'initialValue' => $seqGeneratorAnnotation->initialValue - ]); + ])); } elseif ($customGeneratorAnnotation = $this->reader->getPropertyAnnotation($property, ORM\CustomIdGenerator::class)) { $metadata->setCustomGeneratorDefinition([ 'class' => $customGeneratorAnnotation->class @@ -847,10 +870,10 @@ protected function evaluatePropertyAnnotations(ORM\ClassMetadataInfo $metadata) // Evaluate @Cache annotation if (($cacheAnnotation = $this->reader->getPropertyAnnotation($property, ORM\Cache::class)) !== null) { - $metadata->enableAssociationCache($mapping['fieldName'], [ - 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnotation->usage), - 'region' => $cacheAnnotation->region, - ]); + $metadata->enableAssociationCache($mapping['fieldName'], array_filter([ + 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnotation->usage), + 'region' => $cacheAnnotation->region, + ])); } } } @@ -861,8 +884,8 @@ protected function evaluatePropertyAnnotations(ORM\ClassMetadataInfo $metadata) * @param ORM\JoinTable $joinTableAnnotation * @param \ReflectionProperty $property * @param string $className - * @param array $mapping - * @return array + * @param array $mapping + * @return array{name: string, schema: ?string, joinColumns: array, inverseJoinColumns: array} */ protected function evaluateJoinTableAnnotation(ORM\JoinTable $joinTableAnnotation, \ReflectionProperty $property, $className, array $mapping) { @@ -908,12 +931,12 @@ protected function evaluateJoinTableAnnotation(ORM\JoinTable $joinTableAnnotatio } /** - * Check for and build JoinColummn/JoinColumns annotations. + * Check for and build JoinColumn/JoinColumns annotations. * * If no annotations are found, a default is returned. * * @param \ReflectionProperty $property - * @return array + * @return array> */ protected function evaluateJoinColumnAnnotations(\ReflectionProperty $property) { @@ -933,8 +956,8 @@ protected function evaluateJoinColumnAnnotations(\ReflectionProperty $property) /** * Evaluate the association overrides annotations and amend the metadata accordingly. * - * @param array $classAnnotations - * @param ORM\ClassMetadataInfo $metadata + * @param array $classAnnotations + * @param ORM\ClassMetadataInfo $metadata * @return void */ protected function evaluateOverridesAnnotations(array $classAnnotations, ORM\ClassMetadataInfo $metadata) @@ -992,9 +1015,9 @@ protected function evaluateOverridesAnnotations(array $classAnnotations, ORM\Cla /** * Evaluate the EntityListeners annotation and amend the metadata accordingly. * - * @param \ReflectionClass $class - * @param ORM\ClassMetadata $metadata - * @param array $classAnnotations + * @param \ReflectionClass $class + * @param ORM\ClassMetadata $metadata + * @param array $classAnnotations * @return void * @throws ORM\MappingException */ @@ -1007,7 +1030,7 @@ protected function evaluateEntityListenersAnnotation(\ReflectionClass $class, OR $listenerClassName = $metadata->fullyQualifiedClassName($item); if ($listenerClassName === null || !class_exists($listenerClassName)) { - throw ORM\MappingException::entityListenerClassNotFound($listenerClassName, $class->getName()); + throw ORM\MappingException::entityListenerClassNotFound($listenerClassName ?: 'null', $class->getName()); } $hasMapping = false; @@ -1034,8 +1057,8 @@ protected function evaluateEntityListenersAnnotation(\ReflectionClass $class, OR /** * Evaluate the lifecycle annotations and amend the metadata accordingly. * - * @param \ReflectionClass $class - * @param ORM\ClassMetadataInfo $metadata + * @param \ReflectionClass $class + * @param ORM\ClassMetadataInfo $metadata * @return void */ protected function evaluateLifeCycleAnnotations(\ReflectionClass $class, ORM\ClassMetadataInfo $metadata) @@ -1058,7 +1081,7 @@ protected function evaluateLifeCycleAnnotations(\ReflectionClass $class, ORM\Cla * Returns an array of callbacks for lifecycle annotations on the given method. * * @param \ReflectionMethod $method - * @return array + * @return array> */ protected function getMethodCallbacks(\ReflectionMethod $method) { @@ -1120,7 +1143,7 @@ protected function getMaxIdentifierLength() * Returns whether the class with the specified name is transient. Only non-transient * classes, that is entities and mapped superclasses, should have their metadata loaded. * - * @param string $className + * @param class-string $className * @return boolean */ public function isTransient($className) @@ -1129,7 +1152,7 @@ public function isTransient($className) ( !$this->reflectionService->isClassAnnotatedWith($className, Flow\Entity::class) && !$this->reflectionService->isClassAnnotatedWith($className, Flow\ValueObject::class) && - !$this->reflectionService->isClassAnnotatedWith($className, ORM\Entity::class) && + !$this->reflectionService->isClassAnnotatedWith($className, ORM\Entity::class) && !$this->reflectionService->isClassAnnotatedWith($className, ORM\MappedSuperclass::class) && !$this->reflectionService->isClassAnnotatedWith($className, ORM\Embeddable::class) ); @@ -1138,7 +1161,7 @@ public function isTransient($className) /** * Returns the names of all mapped (non-transient) classes known to this driver. * - * @return array + * @return array */ public function getAllClassNames() { @@ -1169,7 +1192,7 @@ function ($className) { * * @param ORM\JoinColumn $joinColumnAnnotation * @param string $propertyName - * @return array + * @return array{name: ?string, unique: bool, nullable: bool, onDelete: mixed, columnDefinition: ?string, referencedColumnName: string} */ protected function joinColumnToArray(ORM\JoinColumn $joinColumnAnnotation, $propertyName = null) { @@ -1187,9 +1210,9 @@ protected function joinColumnToArray(ORM\JoinColumn $joinColumnAnnotation, $prop * Parse the given Column into an array * * @param ORM\Column $columnAnnotation - * @param array $mapping + * @param array $mapping * @param string $fieldName - * @return array + * @return array */ protected function addColumnToMappingArray(ORM\Column $columnAnnotation, array $mapping = [], $fieldName = null) { @@ -1222,11 +1245,12 @@ protected function addColumnToMappingArray(ORM\Column $columnAnnotation, array $ /** * Returns the classname after stripping a potentially present Compiler::ORIGINAL_CLASSNAME_SUFFIX. * - * @param string $className - * @return string + * @param class-string $className + * @return class-string */ protected function getUnproxiedClassName($className) { + /** @var class-string $className */ $className = preg_replace('/' . Compiler::ORIGINAL_CLASSNAME_SUFFIX . '$/', '', $className); return $className; @@ -1253,7 +1277,7 @@ private function getFetchMode($className, $fetchMode) /** * Checks if the specified class has a property annotated with Id * - * @param string $className Name of the class to check against + * @param class-string $className Name of the class to check against * @param string $methodName Name of the method to check against * @param string $methodDeclaringClassName Name of the class the method was originally declared in * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection. @@ -1284,7 +1308,7 @@ public function hasRuntimeEvaluationsDefinition() /** * Returns runtime evaluations for a previously matched pointcut * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition() { diff --git a/Neos.Flow/Classes/Persistence/Doctrine/MigrationFinder.php b/Neos.Flow/Classes/Persistence/Doctrine/MigrationFinder.php index 93555a066c..ced9438bae 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/MigrationFinder.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/MigrationFinder.php @@ -45,7 +45,7 @@ public function findMigrations(string $directory, ?string $namespace = null): ar $this->databasePlatformName ]); if (is_dir($path)) { - $files[] = glob($path . '/Version*.php'); + $files[] = glob($path . '/Version*.php') ?: []; } } diff --git a/Neos.Flow/Classes/Persistence/Doctrine/ObjectValidationAndDeDuplicationListener.php b/Neos.Flow/Classes/Persistence/Doctrine/ObjectValidationAndDeDuplicationListener.php index 92881f78b5..6732468bea 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/ObjectValidationAndDeDuplicationListener.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/ObjectValidationAndDeDuplicationListener.php @@ -143,7 +143,7 @@ private function deduplicateValueObjectInsertions() * Validates the given object and throws an exception if validation fails. * * @param object $object - * @param \SplObjectStorage $validatedInstancesContainer + * @param \SplObjectStorage $validatedInstancesContainer * @return void * @throws ObjectValidationFailedException */ @@ -151,21 +151,18 @@ private function validateObject($object, \SplObjectStorage $validatedInstancesCo { $className = $this->entityManager->getClassMetadata(get_class($object))->getName(); $validator = $this->validatorResolver->getBaseValidatorConjunction($className, ['Persistence', 'Default']); - if ($validator === null) { - return; - } $validator->setValidatedInstancesContainer($validatedInstancesContainer); $validationResult = $validator->validate($object); - if ($validationResult->hasErrors()) { + if ($validationResult?->hasErrors()) { $errorMessages = ''; $errorCount = 0; - $allErrors = $validationResult->getFlattenedErrors(); + $allErrors = $validationResult->getFlattenedErrors() ?: []; foreach ($allErrors as $path => $errors) { $errorMessages .= $path . ':' . PHP_EOL; foreach ($errors as $error) { $errorCount++; - $errorMessages .= (string)$error . PHP_EOL; + $errorMessages .= $error . PHP_EOL; } } throw new ObjectValidationFailedException('An instance of "' . get_class($object) . '" failed to pass validation with ' . $errorCount . ' error(s): ' . PHP_EOL . $errorMessages, 1322585164); diff --git a/Neos.Flow/Classes/Persistence/Doctrine/PersistenceManager.php b/Neos.Flow/Classes/Persistence/Doctrine/PersistenceManager.php index be1c1127c3..9be04cdcef 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/PersistenceManager.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/PersistenceManager.php @@ -151,7 +151,7 @@ public function clearState(): void */ public function isNewObject($object): bool { - if (!$object instanceof PersistenceMagicInterface) { + if (!($object instanceof PersistenceMagicInterface)) { return true; } diff --git a/Neos.Flow/Classes/Persistence/Doctrine/Query.php b/Neos.Flow/Classes/Persistence/Doctrine/Query.php index 62ddd3ed8a..44592cb81d 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/Query.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/Query.php @@ -64,7 +64,7 @@ class Query implements QueryInterface protected $entityManager; /** - * @var array + * @var array */ protected $settings; @@ -74,7 +74,7 @@ class Query implements QueryInterface protected $constraint; /** - * @var array + * @var array */ protected $orderings = []; @@ -104,7 +104,7 @@ class Query implements QueryInterface protected $parameters; /** - * @var array + * @var ?array */ protected $joins; @@ -140,10 +140,10 @@ public function injectEntityManager(EntityManagerInterface $entityManager) * Injects the Flow settings, the persistence part is kept * for further use. * - * @param array $settings + * @param array $settings * @return void */ - public function injectSettings(array $settings) + public function injectSettings(array $settings): void { $this->settings = $settings['persistence']; } @@ -151,7 +151,7 @@ public function injectSettings(array $settings) /** * @param LoggerInterface $logger */ - public function injectLogger(LoggerInterface $logger) + public function injectLogger(LoggerInterface $logger): void { $this->logger = $logger; } @@ -186,7 +186,7 @@ public function execute(bool $cacheResult = false): QueryResultInterface * Really executes the query on the database. * This should only ever be executed from the QueryResult class. * - * @return array result set + * @return mixed result set * @throws DatabaseException * @throws DatabaseConnectionException * @throws DatabaseStructureException @@ -283,7 +283,7 @@ public function count(): int * 'bar' => \Neos\Flow\Persistence\QueryInterface::ORDER_DESCENDING * ) * - * @param array $orderings The property names to order by + * @param array $orderings The property names to order by * @return QueryInterface * @api */ @@ -304,7 +304,7 @@ public function setOrderings(array $orderings): QueryInterface * 'bar' => \Neos\Flow\Persistence\QueryInterface::ORDER_DESCENDING * ) * - * @return array + * @return array * @api */ public function getOrderings(): array @@ -393,7 +393,7 @@ public function getOffset(): ?int * The constraint used to limit the result set. Returns $this to allow * for chaining (fluid interface) * - * @param object $constraint Some constraint, depending on the backend + * @param object|string $constraint Some constraint, depending on the backend * @return QueryInterface * @api */ @@ -456,7 +456,7 @@ public function logicalOr(mixed $constraint1, mixed ...$constraints) /** * Performs a logical negation of the given constraint * - * @param object $constraint Constraint to negate + * @param object|string $constraint Constraint to negate * @return object * @api */ @@ -618,7 +618,7 @@ public function greaterThanOrEqual(string $propertyName, $operand) /** * Add parameters to the query * - * @param array $parameters + * @param array $parameters * @return void */ public function addParameters($parameters) @@ -632,7 +632,7 @@ public function addParameters($parameters) /** * Gets all defined query parameters for the query being constructed. * - * @return ArrayCollection + * @return ArrayCollection */ public function getParameters() { @@ -717,7 +717,7 @@ public function __wakeup() $this->queryBuilder->where($this->constraint); } - if (is_array($this->orderings)) { + if ($this->orderings !== []) { $aliases = $this->queryBuilder->getRootAliases(); foreach ($this->orderings as $propertyName => $order) { $this->queryBuilder->addOrderBy($aliases[0] . '.' . $propertyName, $order); @@ -732,7 +732,7 @@ public function __wakeup() $this->queryBuilder->setMaxResults($this->limit); $this->queryBuilder->distinct($this->distinct); $this->queryBuilder->setParameters($this->parameters); - unset($this->parameters); + $this->parameters = new ArrayCollection(); } /** diff --git a/Neos.Flow/Classes/Persistence/Doctrine/QueryResult.php b/Neos.Flow/Classes/Persistence/Doctrine/QueryResult.php index e8004a88ab..a97d42307b 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/QueryResult.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/QueryResult.php @@ -23,7 +23,7 @@ class QueryResult implements QueryResultInterface { /** - * @var array + * @var ?array * @Flow\Transient */ protected $rows; @@ -50,6 +50,7 @@ public function __construct(Query $query) /** * Loads the objects this QueryResult is supposed to hold * + * @phpstan-assert array $this->rows * @return void */ protected function initialize() @@ -110,7 +111,7 @@ public function count(): int /** * Returns an array with the objects in the result set * - * @return array + * @return array * @api */ public function toArray(): array @@ -209,6 +210,6 @@ public function rewind(): void public function valid(): bool { $this->initialize(); - return current($this->rows) !== false; + return current($this->rows ?: []) !== false; } } diff --git a/Neos.Flow/Classes/Persistence/Doctrine/Repository.php b/Neos.Flow/Classes/Persistence/Doctrine/Repository.php index 5749463bea..87017f174f 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/Repository.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/Repository.php @@ -13,8 +13,8 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\QueryBuilder; -use Doctrine\Persistence\Mapping\ClassMetadata; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\Exception\IllegalObjectTypeException; use Neos\Flow\Persistence\Exception\UnknownObjectException; @@ -26,6 +26,7 @@ /** * The Flow default Repository, based on Doctrine 2 * + * @extends EntityRepository * @api */ abstract class Repository extends EntityRepository implements RepositoryInterface @@ -50,7 +51,7 @@ abstract class Repository extends EntityRepository implements RepositoryInterfac protected $objectType; /** - * @var array + * @var array */ protected $defaultOrderings = []; @@ -58,7 +59,7 @@ abstract class Repository extends EntityRepository implements RepositoryInterfac * Initializes a new Repository. * * @param EntityManagerInterface $entityManager The EntityManager to use. - * @param ClassMetadata|null $classMetadata The class descriptor. + * @param ClassMetadata|null $classMetadata The class descriptor. */ public function __construct(EntityManagerInterface $entityManager, ?ClassMetadata $classMetadata = null) { @@ -70,8 +71,12 @@ public function __construct(EntityManagerInterface $entityManager, ?ClassMetadat } /** @var class-string $objectType */ $this->objectType = $objectType; + /** @var ?ClassMetadata $classMetadata */ $classMetadata = $entityManager->getClassMetadata($this->objectType); } + if (!$classMetadata) { + throw new \Exception('Unable to resolve class metadata for ' . $this->objectType); + } parent::__construct($entityManager, $classMetadata); $this->entityManager = $this->_em; } @@ -97,9 +102,8 @@ public function getEntityClassName(): string */ public function add($object): void { - if (!is_object($object) || !($object instanceof $this->objectType)) { - $type = (is_object($object) ? get_class($object) : gettype($object)); - throw new IllegalObjectTypeException('The value given to add() was ' . $type . ' , however the ' . get_class($this) . ' can only store ' . $this->objectType . ' instances.', 1517408062); + if (!($object instanceof $this->objectType)) { + throw new IllegalObjectTypeException('The value given to add() was ' . get_class($object) . ' , however the ' . get_class($this) . ' can only store ' . $this->objectType . ' instances.', 1517408062); } $this->entityManager->persist($object); } @@ -114,9 +118,8 @@ public function add($object): void */ public function remove($object): void { - if (!is_object($object) || !($object instanceof $this->objectType)) { - $type = (is_object($object) ? get_class($object) : gettype($object)); - throw new IllegalObjectTypeException('The value given to remove() was ' . $type . ' , however the ' . get_class($this) . ' can only handle ' . $this->objectType . ' instances.', 1517408067); + if (!($object instanceof $this->objectType)) { + throw new IllegalObjectTypeException('The value given to remove() was ' . get_class($object) . ' , however the ' . get_class($this) . ' can only handle ' . $this->objectType . ' instances.', 1517408067); } $this->entityManager->remove($object); } @@ -124,9 +127,10 @@ public function remove($object): void /** * Finds all entities in the repository. * - * @phpstan-ignore-next-line we don't satisfy the contract of doctrines repository as we don't return a simple array. - * @return QueryResultInterface The query result + * @return QueryResultInterface The query result * @api + * (don't know how generics work here yet and ignoring method.childReturnType has no effect) + * @phpstan-ignore-next-line */ public function findAll(): QueryResultInterface { @@ -136,7 +140,7 @@ public function findAll(): QueryResultInterface /** * Find all objects and return an IterableResult * - * @return iterable + * @return iterable */ public function findAllIterator(): iterable { @@ -222,7 +226,7 @@ public function removeAll(): void * 'bar' => \Neos\Flow\Persistence\QueryInterface::ORDER_DESCENDING * ) * - * @param array $defaultOrderings The property names to order by by default + * @param array $defaultOrderings The property names to order by by default * @return void * @api */ @@ -257,7 +261,7 @@ public function update($object): void * - countBy($value, $caseSensitive = true) * * @param string $method Name of the method - * @param array $arguments The arguments + * @param array $arguments The arguments * @return mixed The result of the repository method * @api */ diff --git a/Neos.Flow/Classes/Persistence/Doctrine/Service.php b/Neos.Flow/Classes/Persistence/Doctrine/Service.php index 2c18e36a20..c3d3496588 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/Service.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/Service.php @@ -42,6 +42,7 @@ use Doctrine\Migrations\Version\ExecutionResult; use Doctrine\Migrations\Version\Version; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\MappingException; use Doctrine\ORM\Tools\SchemaTool; use Doctrine\ORM\Tools\SchemaValidator; @@ -97,7 +98,7 @@ class Service * Validates the metadata mapping for Doctrine, using the SchemaValidator * of Doctrine. * - * @return array + * @return array> */ public function validateMapping(): array { @@ -167,7 +168,7 @@ public function compileProxies(): void * Returns information about which entities exist and possibly if their * mapping information contains errors or not. * - * @return array + * @return array> */ public function getEntityStatus(): array { @@ -192,12 +193,12 @@ public function getEntityStatus(): array * Run DQL and return the result as-is. * * @param string $dql - * @param integer $hydrationMode + * @param 1|2|3|4|5|6|string|null $hydrationMode * @param int|null $firstResult * @param int|null $maxResult * @return mixed */ - public function runDql(string $dql, int $hydrationMode = \Doctrine\ORM\Query::HYDRATE_OBJECT, ?int $firstResult = null, ?int $maxResult = null) + public function runDql(string $dql, int|string|null $hydrationMode = \Doctrine\ORM\Query::HYDRATE_OBJECT, ?int $firstResult = null, ?int $maxResult = null) { $query = $this->entityManager->createQuery($dql); if ($firstResult !== null) { @@ -489,7 +490,7 @@ public function markAsMigrated(string $version, bool $markAsMigrated, ?string $o if ($version === 'all') { if ($markAsMigrated === false) { foreach ($executedMigrations->getItems() as $availableMigration) { - $this->mark($output, $availableMigration->getVersion(), false, $executedMigrations, !$markAsMigrated, $overrideMigrationFolderName); + $this->mark($output, $availableMigration->getVersion(), false, $executedMigrations, true, $overrideMigrationFolderName); } } @@ -619,7 +620,7 @@ public function getMigrationStatus(?string $overrideMigrationFolderName = null): * @param boolean $diffAgainstCurrent * @param string|null $filterExpression * @param string|null $overrideMigrationFolderName - * @return array Path to the new file + * @return array Path to the new file * @throws DBALException * @throws FilesException */ @@ -694,10 +695,10 @@ public function getMigrationFolderName(): string * * @param Schema $schema * @param AbstractPlatform $platform - * @param array $tableNames + * @param array $tableNames * @param string $search * @param string $replace - * @return array + * @return array{drop: array, add: array} */ public static function getForeignKeyHandlingSql(Schema $schema, AbstractPlatform $platform, array $tableNames, string $search, string $replace): array { diff --git a/Neos.Flow/Classes/Persistence/EmptyQueryResult.php b/Neos.Flow/Classes/Persistence/EmptyQueryResult.php index ce291f11d6..3f1e5ed430 100644 --- a/Neos.Flow/Classes/Persistence/EmptyQueryResult.php +++ b/Neos.Flow/Classes/Persistence/EmptyQueryResult.php @@ -58,7 +58,7 @@ public function getFirst() /** * Returns an empty array * - * @return array + * @return array * @api */ public function toArray(): array @@ -68,6 +68,7 @@ public function toArray(): array public function current(): mixed { + /** @phpstan-ignore return.type (I don't see any "object" requirement here) */ return null; } diff --git a/Neos.Flow/Classes/Persistence/PersistenceManagerInterface.php b/Neos.Flow/Classes/Persistence/PersistenceManagerInterface.php index 5aff47fa89..80888fb1e6 100644 --- a/Neos.Flow/Classes/Persistence/PersistenceManagerInterface.php +++ b/Neos.Flow/Classes/Persistence/PersistenceManagerInterface.php @@ -22,7 +22,7 @@ interface PersistenceManagerInterface /** * Injects the Flow settings, called by Flow. * - * @param array $settings + * @param array $settings * @return void * @api */ diff --git a/Neos.Flow/Classes/Persistence/QueryInterface.php b/Neos.Flow/Classes/Persistence/QueryInterface.php index 910f37d9d8..f7d6334bae 100644 --- a/Neos.Flow/Classes/Persistence/QueryInterface.php +++ b/Neos.Flow/Classes/Persistence/QueryInterface.php @@ -129,7 +129,7 @@ public function count(): int; * 'bar' => \Neos\Flow\Persistence\QueryInterface::ORDER_DESCENDING * ) * - * @param array $orderings The property names to order by + * @param array $orderings The property names to order by * @return QueryInterface * @api */ @@ -142,7 +142,7 @@ public function setOrderings(array $orderings): QueryInterface; * 'bar' => \Neos\Flow\Persistence\QueryInterface::ORDER_DESCENDING * ) * - * @return array + * @return array * @api */ public function getOrderings(): array; diff --git a/Neos.Flow/Classes/Persistence/QueryResultInterface.php b/Neos.Flow/Classes/Persistence/QueryResultInterface.php index 001d247753..1c260a280f 100644 --- a/Neos.Flow/Classes/Persistence/QueryResultInterface.php +++ b/Neos.Flow/Classes/Persistence/QueryResultInterface.php @@ -14,6 +14,8 @@ /** * A lazy result list that is returned by Query::execute() * + * @extends \Iterator + * @extends \ArrayAccess * @api */ interface QueryResultInterface extends \Countable, \Iterator, \ArrayAccess @@ -37,7 +39,7 @@ public function getFirst(); /** * Returns an array with the objects in the result set * - * @return array + * @return array * @api */ public function toArray(): array; diff --git a/Neos.Flow/Classes/Persistence/Repository.php b/Neos.Flow/Classes/Persistence/Repository.php index cea6418226..3ace4da0cf 100644 --- a/Neos.Flow/Classes/Persistence/Repository.php +++ b/Neos.Flow/Classes/Persistence/Repository.php @@ -36,7 +36,7 @@ abstract class Repository implements RepositoryInterface protected $entityClassName; /** - * @var array + * @var array */ protected $defaultOrderings = []; @@ -78,9 +78,8 @@ public function getEntityClassName(): string */ public function add($object): void { - if (!is_object($object) || !($object instanceof $this->entityClassName)) { - $type = (is_object($object) ? get_class($object) : gettype($object)); - throw new IllegalObjectTypeException('The value given to add() was ' . $type . ' , however the ' . get_class($this) . ' can only store ' . $this->entityClassName . ' instances.', 1298403438); + if (!($object instanceof $this->entityClassName)) { + throw new IllegalObjectTypeException('The value given to add() was ' . get_class($object) . ' , however the ' . get_class($this) . ' can only store ' . $this->entityClassName . ' instances.', 1298403438); } $this->persistenceManager->add($object); } @@ -95,9 +94,8 @@ public function add($object): void */ public function remove($object): void { - if (!is_object($object) || !($object instanceof $this->entityClassName)) { - $type = (is_object($object) ? get_class($object) : gettype($object)); - throw new IllegalObjectTypeException('The value given to remove() was ' . $type . ' , however the ' . get_class($this) . ' can only handle ' . $this->entityClassName . ' instances.', 1298403442); + if (!($object instanceof $this->entityClassName)) { + throw new IllegalObjectTypeException('The value given to remove() was ' . get_class($object) . ' , however the ' . get_class($this) . ' can only handle ' . $this->entityClassName . ' instances.', 1298403442); } $this->persistenceManager->remove($object); } @@ -174,7 +172,7 @@ public function removeAll(): void * 'bar' => \Neos\Flow\Persistence\QueryInterface::ORDER_DESCENDING * ) * - * @param array $defaultOrderings The property names to order by by default + * @param array $defaultOrderings The property names to order by by default * @return void * @api */ @@ -192,9 +190,8 @@ public function setDefaultOrderings(array $defaultOrderings): void */ public function update($object): void { - if (!is_object($object) || !($object instanceof $this->entityClassName)) { - $type = (is_object($object) ? get_class($object) : gettype($object)); - throw new IllegalObjectTypeException('The value given to update() was ' . $type . ' , however the ' . get_class($this) . ' can only store ' . $this->entityClassName . ' instances.', 1249479625); + if (!($object instanceof $this->entityClassName)) { + throw new IllegalObjectTypeException('The value given to update() was ' . get_class($object) . ' , however the ' . get_class($this) . ' can only store ' . $this->entityClassName . ' instances.', 1249479625); } $this->persistenceManager->update($object); @@ -209,7 +206,7 @@ public function update($object): void * - countBy($value, $caseSensitive = true) * * @param string $method Name of the method - * @param array $arguments The arguments + * @param array $arguments The arguments * @return mixed The result of the repository method * @api */ diff --git a/Neos.Flow/Classes/Persistence/RepositoryInterface.php b/Neos.Flow/Classes/Persistence/RepositoryInterface.php index 12dcf762dd..23f4e0c6af 100644 --- a/Neos.Flow/Classes/Persistence/RepositoryInterface.php +++ b/Neos.Flow/Classes/Persistence/RepositoryInterface.php @@ -93,7 +93,7 @@ public function removeAll(): void; * 'bar' => \Neos\Flow\Persistence\QueryInterface::ORDER_DESCENDING * ) * - * @param array $defaultOrderings The property names to order by by default + * @param array $defaultOrderings The property names to order by by default * @return void * @api */ @@ -117,7 +117,7 @@ public function update($object): void; * - countBy($value, $caseSensitive = true) * * @param string $method Name of the method - * @param array $arguments The arguments + * @param array $arguments The arguments * @return mixed The result of the repository method * @api */ diff --git a/Neos.Flow/Classes/Property/PropertyMapper.php b/Neos.Flow/Classes/Property/PropertyMapper.php index 8344fc0d59..8ab371830a 100644 --- a/Neos.Flow/Classes/Property/PropertyMapper.php +++ b/Neos.Flow/Classes/Property/PropertyMapper.php @@ -53,14 +53,14 @@ class PropertyMapper * 3. Dimension: Priority * Value: Type Converter instance * - * @var array + * @var array>>> */ protected $typeConverters = []; /** * A list of property mapping messages (errors, warnings) which have occured on last mapping. * - * @var Result + * @var ?Result */ protected $messages; @@ -85,7 +85,7 @@ public function initializeObject() * Returns all class names implementing the TypeConverterInterface. * * @param ObjectManagerInterface $objectManager - * @return array Array of type converter implementations + * @return array> Array of type converter implementations * @Flow\CompileStatic */ public static function getTypeConverterImplementationClassNames($objectManager) @@ -106,9 +106,10 @@ public static function getTypeConverterImplementationClassNames($objectManager) * @return mixed an instance of $targetType * @throws Exception * @throws SecurityException + * @phpstan-assert Result $this->getMessages() * @api */ - public function convert($source, $targetType, ?PropertyMappingConfigurationInterface $configuration = null) + public function convert($source, string $targetType, ?PropertyMappingConfigurationInterface $configuration = null) { if ($configuration === null) { $configuration = $this->buildPropertyMappingConfiguration(); @@ -123,9 +124,10 @@ public function convert($source, $targetType, ?PropertyMappingConfigurationInter } return $result; - } catch (SecurityException $exception) { + } catch (SecurityException $exception) { /** @phpstan-ignore catch.neverThrown (AOP I guess) */ throw $exception; } catch (\Exception $exception) { + /** @phpstan-ignore greater.alwaysFalse (Not sure about this tbh) */ throw new PropertyException('Could not convert target type "' . $targetType . '"' . (count($currentPropertyPath) > 0 ? ', at property path "' . implode('.', $currentPropertyPath) . '"' : '') . ': ' . $exception->getMessage(), 1297759968, $exception); } } @@ -133,7 +135,7 @@ public function convert($source, $targetType, ?PropertyMappingConfigurationInter /** * Get the messages of the last Property Mapping * - * @return Result + * @return ?Result * @api */ public function getMessages() @@ -147,7 +149,7 @@ public function getMessages() * @param mixed $source the source data to map. MUST be a simple type, NO object allowed! * @param string $targetType The type of the target; can be either a class name or a simple type. * @param PropertyMappingConfigurationInterface $configuration Configuration for the property mapping. - * @param array $currentPropertyPath The property path currently being mapped; used for knowing the context in case an exception is thrown. + * @param array $currentPropertyPath The property path currently being mapped; used for knowing the context in case an exception is thrown. * @return mixed an instance of $targetType * @throws Exception\TypeConverterException * @throws Exception\InvalidPropertyMappingConfigurationException @@ -175,10 +177,6 @@ protected function doMapping($source, $targetType, PropertyMappingConfigurationI $typeConverter = $this->findTypeConverter($source, $targetType, $configuration); $targetType = $typeConverter->getTargetTypeForSource($source, $targetType, $configuration); - if (!is_object($typeConverter) || !($typeConverter instanceof TypeConverterInterface)) { - throw new Exception\TypeConverterException('Type converter for "' . $source . '" -> "' . $targetType . '" not found.'); - } - $convertedChildProperties = []; foreach ($typeConverter->getSourceChildPropertiesToBeConverted($source) as $sourcePropertyName => $sourcePropertyValue) { $targetPropertyName = $configuration->getTargetPropertyName($sourcePropertyName); @@ -210,7 +208,7 @@ protected function doMapping($source, $targetType, PropertyMappingConfigurationI $result = $typeConverter->convertFrom($source, $targetType, $convertedChildProperties, $configuration); if ($result instanceof Error) { - $this->messages->forProperty(implode('.', $currentPropertyPath))->addError($result); + $this->messages?->forProperty(implode('.', $currentPropertyPath))->addError($result); } return $result; @@ -232,9 +230,6 @@ protected function findTypeConverter($source, $targetType, PropertyMappingConfig return $configuration->getTypeConverter(); } - if (!is_string($targetType)) { - throw new Exception\InvalidTargetException('The target type was no string, but of type "' . gettype($targetType) . '"', 1297941727); - } $normalizedTargetType = TypeHandling::normalizeType($targetType); $truncatedTargetType = TypeHandling::truncateElementType($normalizedTargetType); $converter = null; @@ -296,7 +291,7 @@ protected function findFirstEligibleTypeConverterInObjectHierarchy($source, $sou } } - $converters = $this->getConvertersForInterfaces($convertersForSource, class_implements($targetClass)); + $converters = $this->getConvertersForInterfaces($convertersForSource, class_implements($targetClass) ?: []); $converter = $this->findEligibleConverterWithHighestPriority($converters, $source, $targetType); if ($converter !== null) { @@ -341,9 +336,9 @@ protected function findEligibleConverterWithHighestPriority($converters, $source } /** - * @param array $convertersForSource - * @param array $interfaceNames - * @return array + * @param array>> $convertersForSource + * @param array $interfaceNames + * @return array> * @throws DuplicateTypeConverterException */ protected function getConvertersForInterfaces(array $convertersForSource, array $interfaceNames) @@ -353,7 +348,7 @@ protected function getConvertersForInterfaces(array $convertersForSource, array if (isset($convertersForSource[$implementedInterface])) { foreach ($convertersForSource[$implementedInterface] as $priority => $converter) { if (isset($convertersForInterface[$priority])) { - throw new DuplicateTypeConverterException('There exist at least two converters which handle the conversion to an interface with priority "' . $priority . '". ' . get_class($convertersForInterface[$priority]) . ' and ' . get_class($converter), 1297951338); + throw new DuplicateTypeConverterException('There exist at least two converters which handle the conversion to an interface with priority "' . $priority . '". ' . $convertersForInterface[$priority] . ' and ' . $converter, 1297951338); } $convertersForInterface[$priority] = $converter; } @@ -367,7 +362,7 @@ protected function getConvertersForInterfaces(array $convertersForSource, array * Determine the type of the source data, or throw an exception if source was an unsupported format. * * @param mixed $source - * @return array Possible source types (single value for simple typed source, multiple values for object source) + * @return array Possible source types (single value for simple typed source, multiple values for object source) * @throws Exception\InvalidSourceException */ protected function determineSourceTypes($source) @@ -396,7 +391,7 @@ protected function determineSourceTypes($source) /** * Collects all TypeConverter implementations in a multi-dimensional array with source and target types. * - * @return array + * @return array>>> * @throws Exception\DuplicateTypeConverterException * @see getTypeConverters */ @@ -405,7 +400,6 @@ protected function prepareTypeConverterMap() $typeConverterMap = []; $typeConverterClassNames = static::getTypeConverterImplementationClassNames($this->objectManager); foreach ($typeConverterClassNames as $typeConverterClassName) { - /** @var TypeConverterInterface $typeConverter */ $typeConverter = $this->objectManager->get($typeConverterClassName); foreach ($typeConverter->getSupportedSourceTypes() as $supportedSourceType) { $normalizedSourceType = TypeHandling::normalizeType($supportedSourceType); @@ -430,7 +424,7 @@ protected function prepareTypeConverterMap() * 3. Dimension: Priority * Value: Type Converter instance * - * @return array + * @return array>>> */ public function getTypeConverters() { diff --git a/Neos.Flow/Classes/Property/PropertyMappingConfiguration.php b/Neos.Flow/Classes/Property/PropertyMappingConfiguration.php index dc6021c1cb..c92713b00f 100644 --- a/Neos.Flow/Classes/Property/PropertyMappingConfiguration.php +++ b/Neos.Flow/Classes/Property/PropertyMappingConfiguration.php @@ -30,21 +30,21 @@ class PropertyMappingConfiguration implements PropertyMappingConfigurationInterf * 2. Dimension: Configuration Key * Value: Configuration Value * - * @var array + * @var array,array> */ protected $configuration; /** * Stores the configuration for specific child properties. * - * @var array + * @var array */ protected $subConfigurationForProperty = []; /** * Keys which should be renamed * - * @var array + * @var array */ protected $mapping = []; @@ -56,21 +56,21 @@ class PropertyMappingConfiguration implements PropertyMappingConfigurationInterf /** * List of allowed property names to be converted * - * @var array + * @var array */ protected $propertiesToBeMapped = []; /** * List of property names to be skipped during property mapping * - * @var array + * @var array */ protected $propertiesToSkip = []; /** * List of disallowed property names which will be ignored while property mapping * - * @var array + * @var array */ protected $propertiesNotToBeMapped = []; @@ -260,7 +260,7 @@ public function getTargetPropertyName($sourcePropertyName) /** * @param string $typeConverterClassName - * @param string $key + * @param string|int $key * @return mixed configuration value for the specific $typeConverterClassName. Can be used by Type Converters to fetch converter-specific configuration. * @api */ @@ -290,8 +290,8 @@ public function setMapping($sourcePropertyName, $targetPropertyName) /** * Set all options for the given $typeConverter. * - * @param string $typeConverter class name of type converter - * @param array $options + * @param class-string $typeConverter class name of type converter + * @param array $options * @return PropertyMappingConfiguration this * @api */ @@ -306,7 +306,7 @@ public function setTypeConverterOptions($typeConverter, array $options) /** * Set a single option (denoted by $optionKey) for the given $typeConverter. * - * @param string $typeConverter class name of type converter + * @param class-string $typeConverter class name of type converter * @param int|string $optionKey * @param mixed $optionValue * @return PropertyMappingConfiguration this @@ -326,13 +326,18 @@ public function setTypeConverterOption($typeConverter, $optionKey, $optionValue) * When setting an option on a subclassed type converter, this option must also be set on * all its parent type converters. * - * @param string $typeConverter The type converter class - * @return array Class names of type converters + * @param class-string $typeConverter The type converter class + * @return array> Class names of type converters */ protected function getTypeConvertersWithParentClasses($typeConverter) { $typeConverterClasses = class_parents($typeConverter); - $typeConverterClasses = $typeConverterClasses === false ? [] : $typeConverterClasses; + $typeConverterClasses = $typeConverterClasses === false + ? [] + : array_filter( + $typeConverterClasses, + fn (string $parentClassName): bool => is_subclass_of($parentClassName, TypeConverterInterface::class) + ); $typeConverterClasses[] = $typeConverter; return $typeConverterClasses; } @@ -355,7 +360,7 @@ public function forProperty($propertyPath) /** * Traverse the property configuration. Only used by forProperty(). * - * @param array $splittedPropertyPath + * @param array $splittedPropertyPath * @return PropertyMappingConfiguration (or a subclass thereof) */ public function traverseProperties(array $splittedPropertyPath) diff --git a/Neos.Flow/Classes/Property/PropertyMappingConfigurationInterface.php b/Neos.Flow/Classes/Property/PropertyMappingConfigurationInterface.php index 786e08b003..55622e78c7 100644 --- a/Neos.Flow/Classes/Property/PropertyMappingConfigurationInterface.php +++ b/Neos.Flow/Classes/Property/PropertyMappingConfigurationInterface.php @@ -70,7 +70,7 @@ public function getTargetPropertyName($sourcePropertyName); /** * @param string $typeConverterClassName - * @param string $key + * @param string|int $key * @return mixed configuration value for the specific $typeConverterClassName. Can be used by Type Converters to fetch converter-specific configuration * @api */ diff --git a/Neos.Flow/Classes/Property/TypeConverter/ArrayConverter.php b/Neos.Flow/Classes/Property/TypeConverter/ArrayConverter.php index ca088c34cb..216f8b2bc8 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/ArrayConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/ArrayConverter.php @@ -112,9 +112,9 @@ class ArrayConverter extends AbstractTypeConverter * * @param mixed $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration - * @return array + * @return array * @throws InvalidPropertyMappingConfigurationException * @throws InvalidSourceException * @throws TypeConverterException @@ -146,9 +146,13 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie $exportType = $this->getResourceExportType($configuration); switch ($exportType) { case self::RESOURCE_EXPORT_TYPE_BASE64: + $fileContent = file_get_contents('resource://' . $source->getSha1()); + if ($fileContent === false) { + throw new \Exception('Failed to fetch content for resource ' . $source->getSha1(), 1744060115); + } return [ 'filename' => $source->getFilename(), - 'data' => base64_encode(file_get_contents('resource://' . $source->getSha1())), + 'data' => base64_encode($fileContent), 'collectionName' => $source->getCollectionName(), 'relativePublicationPath' => $source->getRelativePublicationPath(), 'mediaType' => $source->getMediaType(), @@ -159,7 +163,13 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie if ($sourceStream === false) { throw new InvalidSourceException(sprintf('Could not get stream of resource "%s" (%s). This might be caused by a broken resource object and can be fixed by running the "resource:clean" command.', $source->getFilename(), $source->getSha1()), 1435842312); } + if (!$configuration) { + throw new \Exception('Cannot resolve target path without a property mapping configuration', 1744060031); + } $targetStream = fopen($configuration->getConfigurationValue(ArrayConverter::class, self::CONFIGURATION_RESOURCE_SAVE_PATH) . '/' . $source->getSha1(), 'w'); + if ($targetStream === false) { + throw new \Exception('Could not open target stream', 1744060053); + } stream_copy_to_stream($sourceStream, $targetStream); fclose($targetStream); fclose($sourceStream); @@ -181,7 +191,7 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie /** * @param PropertyMappingConfigurationInterface|null $configuration - * @return string + * @return non-empty-string * @throws InvalidPropertyMappingConfigurationException */ protected function getStringDelimiter(?PropertyMappingConfigurationInterface $configuration = null) @@ -195,6 +205,8 @@ protected function getStringDelimiter(?PropertyMappingConfigurationInterface $co return self::DEFAULT_STRING_DELIMITER; } elseif (!is_string($stringDelimiter)) { throw new InvalidPropertyMappingConfigurationException(sprintf('CONFIGURATION_STRING_DELIMITER must be of type string, "%s" given', (is_object($stringDelimiter) ? get_class($stringDelimiter) : gettype($stringDelimiter))), 1368433339); + } elseif ($stringDelimiter === '') { + throw new InvalidPropertyMappingConfigurationException('CONFIGURATION_STRING_DELIMITER must not be empty', 1744060278); } return $stringDelimiter; diff --git a/Neos.Flow/Classes/Property/TypeConverter/ArrayFromObjectConverter.php b/Neos.Flow/Classes/Property/TypeConverter/ArrayFromObjectConverter.php index 0c5d63e1d6..772798dbac 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/ArrayFromObjectConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/ArrayFromObjectConverter.php @@ -50,7 +50,7 @@ class ArrayFromObjectConverter extends AbstractTypeConverter * Convert all properties in the source array * * @param mixed $source - * @return array + * @return array */ public function getSourceChildPropertiesToBeConverted($source) { @@ -90,7 +90,7 @@ public function getTypeOfChildProperty($targetType, $propertyName, PropertyMappi * * @param mixed $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return mixed|Error the target type, or an error object if a user-error occurred * @api diff --git a/Neos.Flow/Classes/Property/TypeConverter/ArrayObjectConverter.php b/Neos.Flow/Classes/Property/TypeConverter/ArrayObjectConverter.php index ea519af032..24bfc8c087 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/ArrayObjectConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/ArrayObjectConverter.php @@ -45,9 +45,9 @@ class ArrayObjectConverter extends AbstractTypeConverter * * @param mixed $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration - * @return array + * @return array * @throws InvalidSourceException * @api */ diff --git a/Neos.Flow/Classes/Property/TypeConverter/BooleanConverter.php b/Neos.Flow/Classes/Property/TypeConverter/BooleanConverter.php index 460f1b3541..135b17ab15 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/BooleanConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/BooleanConverter.php @@ -46,7 +46,7 @@ class BooleanConverter extends AbstractTypeConverter * * @param mixed $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return boolean * @api diff --git a/Neos.Flow/Classes/Property/TypeConverter/CollectionConverter.php b/Neos.Flow/Classes/Property/TypeConverter/CollectionConverter.php index 91755c7d9a..6226dc8fcf 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/CollectionConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/CollectionConverter.php @@ -49,9 +49,9 @@ class CollectionConverter extends AbstractTypeConverter * * @param mixed $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration - * @return ArrayCollection + * @return ArrayCollection * @api */ public function convertFrom($source, $targetType, array $convertedChildProperties = [], ?PropertyMappingConfigurationInterface $configuration = null) @@ -63,7 +63,7 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie * Returns the source, if it is an array, otherwise an empty array. * * @param mixed $source - * @return array + * @return array * @api */ public function getSourceChildPropertiesToBeConverted($source) diff --git a/Neos.Flow/Classes/Property/TypeConverter/DateTimeConverter.php b/Neos.Flow/Classes/Property/TypeConverter/DateTimeConverter.php index 8a68a92286..339b3dce97 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/DateTimeConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/DateTimeConverter.php @@ -92,7 +92,7 @@ class DateTimeConverter extends AbstractTypeConverter /** * If conversion is possible. * - * @param string|int|array $source + * @param string|int|array $source * @param string $targetType * @return boolean */ @@ -101,21 +101,16 @@ public function canConvertFrom($source, $targetType) if (!is_callable([$targetType, 'createFromFormat'])) { return false; } - if (is_array($source)) { - return true; - } - if (is_integer($source)) { - return true; - } - return is_string($source); + + return true; } /** * Converts $source to a \DateTime using the configured dateFormat * - * @param string|integer|array $source the string to be converted to a \DateTime object + * @param string|integer|array $source the string to be converted to a \DateTime object * @param string $targetType must be "DateTime" - * @param array $convertedChildProperties not used currently + * @param array $convertedChildProperties not used currently * @param PropertyMappingConfigurationInterface|null $configuration * @return \DateTimeInterface|null|Error * @throws InvalidPropertyMappingConfigurationException @@ -171,7 +166,7 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie if ($date === false) { return new Error('The date "%s" was not recognized (for format "%s").', 1307719788, [$dateAsString, $dateFormat]); } - if (isset($source['hour'], $source['minute'], $source['second']) && is_array($source)) { + if (isset($source['hour'], $source['minute'], $source['second'])) { $date = $this->overrideTime($date, $source); } return $date; @@ -179,7 +174,7 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie /** * Returns whether date information (day, month, year) are present as keys in $source. - * @param array $source + * @param array $source * @return bool */ protected function isDatePartKeysProvided(array $source) @@ -215,7 +210,7 @@ protected function getDefaultDateFormat(?PropertyMappingConfigurationInterface $ * Overrides hour, minute & second of the given date with the values in the $source array * * @param \DateTimeInterface $date - * @param array $source + * @param array $source * @return \DateTimeInterface */ protected function overrideTime(\DateTimeInterface $date, array $source) @@ -223,7 +218,7 @@ protected function overrideTime(\DateTimeInterface $date, array $source) $hour = isset($source['hour']) ? (integer)$source['hour'] : 0; $minute = isset($source['minute']) ? (integer)$source['minute'] : 0; $second = isset($source['second']) ? (integer)$source['second'] : 0; - if ($date instanceof \DateTime || $date instanceof \DateTimeImmutable) { + if (is_callable([$date, 'setTime'])) { $date = $date->setTime($hour, $minute, $second); } return $date; diff --git a/Neos.Flow/Classes/Property/TypeConverter/FloatConverter.php b/Neos.Flow/Classes/Property/TypeConverter/FloatConverter.php index 5cd49c1183..d8919f9e9e 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/FloatConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/FloatConverter.php @@ -124,7 +124,7 @@ class FloatConverter extends AbstractTypeConverter * * @param mixed $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return float|Error|null * @throws InvalidPropertyMappingConfigurationException @@ -172,7 +172,7 @@ protected function parseUsingLocaleIfConfigured($source, PropertyMappingConfigur } if (!$locale instanceof Locale) { - $exceptionMessage = 'Determined locale is not of type "\Neos\Flow\I18n\Locale", but of type "' . (is_object($locale) ? get_class($locale) : gettype($locale)) . '".'; + $exceptionMessage = 'Determined locale is not of type "\Neos\Flow\I18n\Locale", but of type "' . gettype($locale) . '".'; throw new InvalidPropertyMappingConfigurationException($exceptionMessage, 1334837413); } @@ -215,8 +215,8 @@ protected function parseUsingLocaleIfConfigured($source, PropertyMappingConfigur * Helper method to collect configuration for this class. * * @param PropertyMappingConfigurationInterface $configuration - * @param array $configurationKeys - * @return array + * @param array $configurationKeys + * @return array */ protected function getConfigurationKeysAndValues(PropertyMappingConfigurationInterface $configuration, array $configurationKeys) { diff --git a/Neos.Flow/Classes/Property/TypeConverter/IntegerConverter.php b/Neos.Flow/Classes/Property/TypeConverter/IntegerConverter.php index 6ccf96a562..edecebe380 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/IntegerConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/IntegerConverter.php @@ -47,7 +47,7 @@ class IntegerConverter extends AbstractTypeConverter * * @param int|string|\DateTimeInterface|null $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return integer|null|Error * @api diff --git a/Neos.Flow/Classes/Property/TypeConverter/MediaTypeConverter.php b/Neos.Flow/Classes/Property/TypeConverter/MediaTypeConverter.php index f78b1cd663..96d4f58d31 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/MediaTypeConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/MediaTypeConverter.php @@ -48,9 +48,9 @@ class MediaTypeConverter extends AbstractTypeConverter implements MediaTypeConve * * @param string $source the raw request body * @param string $targetType must be "array" - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration - * @return array|string|integer Note that this TypeConverter may return a non-array in case of JSON media type, even though he declares to only convert to array + * @return array|string|integer Note that this TypeConverter may return a non-array in case of JSON media type, even though he declares to only convert to array * @api */ public function convertFrom($source, $targetType, array $convertedChildProperties = [], ?PropertyMappingConfigurationInterface $configuration = null) @@ -71,13 +71,13 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie * * @param string $requestBody the raw request body * @param string $mediaType the configured media type (for example "application/json") - * @return array|string|integer + * @return array|string|integer * @api */ protected function convertMediaType($requestBody, $mediaType) { $mediaTypeParts = MediaTypes::parseMediaType($mediaType); - if (!isset($mediaTypeParts['subtype']) || $mediaTypeParts['subtype'] === '') { + if ($mediaTypeParts['subtype'] === '') { return []; } $result = []; diff --git a/Neos.Flow/Classes/Property/TypeConverter/ObjectConverter.php b/Neos.Flow/Classes/Property/TypeConverter/ObjectConverter.php index 62f6599243..77a33e5657 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/ObjectConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/ObjectConverter.php @@ -79,7 +79,18 @@ class ObjectConverter extends AbstractTypeConverter /** * As it is very likely that the constructor arguments are needed twice we should cache them for the request. * - * @var array + * @var array>, + * }>> */ protected $constructorReflectionFirstLevelCache = []; @@ -93,9 +104,12 @@ class ObjectConverter extends AbstractTypeConverter public function canConvertFrom($source, $targetType) { return !( - $this->reflectionService->isClassAnnotatedWith($targetType, Flow\Entity::class) || - $this->reflectionService->isClassAnnotatedWith($targetType, Flow\ValueObject::class) || - $this->reflectionService->isClassAnnotatedWith($targetType, \Doctrine\ORM\Mapping\Entity::class) + class_exists($targetType) && + ( + $this->reflectionService->isClassAnnotatedWith($targetType, Flow\Entity::class) || + $this->reflectionService->isClassAnnotatedWith($targetType, Flow\ValueObject::class) || + $this->reflectionService->isClassAnnotatedWith($targetType, \Doctrine\ORM\Mapping\Entity::class) + ) ); } @@ -116,7 +130,7 @@ public function getSourceChildPropertiesToBeConverted($source) /** * The type of a property is determined by the reflection service. * - * @param string $targetType + * @param class-string $targetType * @param string $propertyName * @param PropertyMappingConfigurationInterface $configuration * @return string|null @@ -149,6 +163,9 @@ public function getTypeOfChildProperty($targetType, $propertyName, PropertyMappi // would not find a property mapper. It is needed because the ObjectConverter doesn't use class schemata, // but reads the annotations directly. $declaredType = strtok(trim(current($varTagValues), " \n\t"), " \n\t"); + if ($declaredType === false) { + throw new \Exception('Invalid declared type', 1744058940); + } try { $parsedType = TypeHandling::parseType($declaredType); } catch (InvalidTypeException $exception) { @@ -173,7 +190,7 @@ public function getTypeOfChildProperty($targetType, $propertyName, PropertyMappi * * @param mixed $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return object the target type * @throws InvalidTargetException @@ -185,6 +202,7 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie $object = $this->buildObject($convertedChildProperties, $targetType); foreach ($convertedChildProperties as $propertyName => $propertyValue) { $result = ObjectAccess::setProperty($object, $propertyName, $propertyValue); + /** @var object $object */ if ($result === false) { $exceptionMessage = sprintf( 'Property "%s" having a value of type "%s" could not be set in target object of type "%s". Make sure that the property is accessible properly, for example via an appropriate setter method.', @@ -214,7 +232,7 @@ public function getTargetTypeForSource($source, $originalTargetType, ?PropertyMa { $targetType = $originalTargetType; - if (is_array($source) && array_key_exists('__type', $source)) { + if (is_array($source) && array_key_exists('__type', $source) && is_string($source['__type'])) { $targetType = $source['__type']; if ($configuration === null) { @@ -224,6 +242,7 @@ public function getTargetTypeForSource($source, $originalTargetType, ?PropertyMa throw new InvalidPropertyMappingConfigurationException('Override of target type not allowed. To enable this, you need to set the PropertyMappingConfiguration Value "CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED" to true.', 1317050430); } + /** @todo is_a is not recursive! replace with is_subclass_of or similar */ if ($targetType !== $originalTargetType && is_a($targetType, $originalTargetType, true) === false) { throw new InvalidDataTypeException('The given type "' . $targetType . '" is not a subtype of "' . $originalTargetType . '".', 1317048056); } @@ -239,7 +258,7 @@ public function getTargetTypeForSource($source, $originalTargetType, ?PropertyMa * * Furthermore, the constructor arguments are removed from $possibleConstructorArgumentValues * - * @param array &$possibleConstructorArgumentValues + * @param array &$possibleConstructorArgumentValues * @param string $objectType * @return object The created instance * @throws InvalidTargetException if a required constructor argument is missing @@ -248,6 +267,9 @@ protected function buildObject(array &$possibleConstructorArgumentValues, $objec { $constructorArguments = []; $className = $this->objectManager->getClassNameByObjectName($objectType); + if ($className === false) { + throw new \Exception('Invalid object type '. $objectType, 1744058576); + } $constructorSignature = $this->getConstructorArgumentsForClass($className); if (count($constructorSignature)) { foreach ($constructorSignature as $constructorArgumentName => $constructorArgumentReflection) { @@ -256,7 +278,7 @@ protected function buildObject(array &$possibleConstructorArgumentValues, $objec unset($possibleConstructorArgumentValues[$constructorArgumentName]); } elseif ($constructorArgumentReflection['optional'] === true) { $constructorArguments[] = $constructorArgumentReflection['defaultValue']; - } elseif ($this->objectManager->isRegistered($constructorArgumentReflection['type']) && $this->objectManager->getScope($constructorArgumentReflection['type']) === Configuration::SCOPE_SINGLETON) { + } elseif ($constructorArgumentReflection['type'] && $this->objectManager->isRegistered($constructorArgumentReflection['type']) && $this->objectManager->getScope($constructorArgumentReflection['type']) === Configuration::SCOPE_SINGLETON) { $constructorArguments[] = $this->objectManager->get($constructorArgumentReflection['type']); } else { throw new InvalidTargetException('Missing constructor argument "' . $constructorArgumentName . '" for object of type "' . $objectType . '".', 1268734872); @@ -272,8 +294,19 @@ protected function buildObject(array &$possibleConstructorArgumentValues, $objec /** * Get the constructor argument reflection for the given object type. * - * @param string $className - * @return array + * @param class-string $className + * @return array>, + * }> */ protected function getConstructorArgumentsForClass($className) { diff --git a/Neos.Flow/Classes/Property/TypeConverter/PersistentObjectConverter.php b/Neos.Flow/Classes/Property/TypeConverter/PersistentObjectConverter.php index 9ce89473ea..8f4bd51253 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/PersistentObjectConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/PersistentObjectConverter.php @@ -88,9 +88,12 @@ class PersistentObjectConverter extends ObjectConverter public function canConvertFrom($source, $targetType) { return ( - $this->reflectionService->isClassAnnotatedWith($targetType, Flow\Entity::class) || - $this->reflectionService->isClassAnnotatedWith($targetType, ValueObject::class) || - $this->reflectionService->isClassAnnotatedWith($targetType, \Doctrine\ORM\Mapping\Entity::class) + class_exists($targetType) && + ( + $this->reflectionService->isClassAnnotatedWith($targetType, Flow\Entity::class) || + $this->reflectionService->isClassAnnotatedWith($targetType, ValueObject::class) || + $this->reflectionService->isClassAnnotatedWith($targetType, \Doctrine\ORM\Mapping\Entity::class) + ) ); } @@ -114,7 +117,7 @@ public function getSourceChildPropertiesToBeConverted($source) /** * The type of a property is determined by the reflection service. * - * @param string $targetType + * @param class-string $targetType * @param string $propertyName * @param PropertyMappingConfigurationInterface $configuration * @return string @@ -128,6 +131,9 @@ public function getTypeOfChildProperty($targetType, $propertyName, PropertyMappi } $schema = $this->reflectionService->getClassSchema($targetType); + if (!$schema) { + throw new InvalidTargetException('Schema for target object of type "' . $targetType . '" not found.', 1744057928); + } $setterMethodName = ObjectAccess::buildSetterMethodName($propertyName); $constructorParameters = $this->reflectionService->getMethodParameters($targetType, '__construct'); @@ -154,7 +160,7 @@ public function getTypeOfChildProperty($targetType, $propertyName, PropertyMappi * * @param mixed $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface $configuration * @return object|TargetNotFoundError|null the converted entity/value object or an instance of TargetNotFoundError if the object could not be resolved * @throws \InvalidArgumentException|InvalidTargetException @@ -191,6 +197,7 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie foreach ($convertedChildProperties as $propertyName => $propertyValue) { // We need to check for "immutable" constructor arguments that have no setter and remove them. + /** @var object $object */ if (isset($objectConstructorArguments[$propertyName]) && !ObjectAccess::isPropertySettable($object, $propertyName)) { $currentPropertyValue = ObjectAccess::getProperty($object, $propertyName); if ($currentPropertyValue === $propertyValue) { @@ -217,15 +224,16 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie } } + /** @var object $object */ return $object; } /** * Handle the case if $source is an array. * - * @param array $source + * @param array $source * @param class-string $targetType - * @param array $convertedChildProperties + * @param array &$convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return object|TargetNotFoundError * @throws InvalidPropertyMappingConfigurationException @@ -267,7 +275,7 @@ protected function handleArrayData(array $source, $targetType, array &$converted * Set the given $identity on the created $object. * * @param object $object - * @param string|array $identity + * @param string|array $identity * @return void * @todo set identity properly if it is composite or custom property */ @@ -300,8 +308,8 @@ protected function fetchObjectFromPersistence($identity, $targetType) /** * Finds an object from the repository by searching for its identity properties. * - * @param array $identityProperties Property names and values to search for - * @param string $type The object type to look for + * @param array $identityProperties Property names and values to search for + * @param class-string $type The object type to look for * @return object|null Either the object matching the identity or NULL if no object was found * @throws DuplicateObjectException if more than one object was found */ @@ -311,7 +319,7 @@ protected function findObjectByIdentityProperties(array $identityProperties, $ty $classSchema = $this->reflectionService->getClassSchema($type); $equals = []; - foreach ($classSchema->getIdentityProperties() as $propertyName => $propertyType) { + foreach ($classSchema?->getIdentityProperties() ?: [] as $propertyName => $propertyType) { if (isset($identityProperties[$propertyName])) { if ($propertyType === 'string') { $equals[] = $query->equals($propertyName, $identityProperties[$propertyName], false); diff --git a/Neos.Flow/Classes/Property/TypeConverter/PersistentObjectSerializer.php b/Neos.Flow/Classes/Property/TypeConverter/PersistentObjectSerializer.php index 5231260403..0ee57cbfde 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/PersistentObjectSerializer.php +++ b/Neos.Flow/Classes/Property/TypeConverter/PersistentObjectSerializer.php @@ -52,7 +52,7 @@ class PersistentObjectSerializer extends AbstractTypeConverter * * @param object $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return mixed The identifier for the object if it is known, or NULL */ diff --git a/Neos.Flow/Classes/Property/TypeConverter/ScalarTypeToObjectConverter.php b/Neos.Flow/Classes/Property/TypeConverter/ScalarTypeToObjectConverter.php index 988e0a5ad4..4656aa035f 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/ScalarTypeToObjectConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/ScalarTypeToObjectConverter.php @@ -58,6 +58,9 @@ class ScalarTypeToObjectConverter extends AbstractTypeConverter */ public function canConvertFrom($source, $targetType) { + if (!class_exists($targetType)) { + return false; + } if (( $this->reflectionService->isClassAnnotatedWith($targetType, Flow\Entity::class) || $this->reflectionService->isClassAnnotatedWith($targetType, Flow\ValueObject::class) || @@ -71,7 +74,8 @@ public function canConvertFrom($source, $targetType) return false; } $methodParameter = array_shift($methodParameters); - return TypeHandling::normalizeType($methodParameter['type']) === TypeHandling::normalizeType(gettype($source)); + $methodParameterType = $methodParameter['type'] ?? null; + return ($methodParameterType ? TypeHandling::normalizeType($methodParameterType) : null) === TypeHandling::normalizeType(gettype($source)); } /** @@ -79,7 +83,7 @@ public function canConvertFrom($source, $targetType) * * @param string|float|integer|bool $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return object */ diff --git a/Neos.Flow/Classes/Property/TypeConverter/SessionConverter.php b/Neos.Flow/Classes/Property/TypeConverter/SessionConverter.php index 3729434ed9..72ab19178b 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/SessionConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/SessionConverter.php @@ -14,6 +14,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Property\PropertyMappingConfigurationInterface; use Neos\Flow\Session\Session; +use Neos\Flow\Session\SessionInterface; use Neos\Flow\Session\SessionManagerInterface; /** @@ -70,9 +71,9 @@ public function canConvertFrom($source, $targetType) * * @param string $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration - * @return object the target type + * @return ?SessionInterface the target type */ public function convertFrom($source, $targetType, array $convertedChildProperties = [], ?PropertyMappingConfigurationInterface $configuration = null) { diff --git a/Neos.Flow/Classes/Property/TypeConverter/StringConverter.php b/Neos.Flow/Classes/Property/TypeConverter/StringConverter.php index dce5764954..ee58bc43ab 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/StringConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/StringConverter.php @@ -94,7 +94,7 @@ class StringConverter extends AbstractTypeConverter * * @param mixed $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return string * @throws InvalidPropertyMappingConfigurationException @@ -113,7 +113,7 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie case self::ARRAY_FORMAT_CSV: return implode($this->getCsvDelimiter($configuration), $source); case self::ARRAY_FORMAT_JSON: - return json_encode($source); + return json_encode($source, JSON_THROW_ON_ERROR); default: throw new InvalidPropertyMappingConfigurationException(sprintf('Invalid array export format "%s" given', $this->getArrayFormat($configuration)), 1404317220); } diff --git a/Neos.Flow/Classes/Property/TypeConverter/TypedArrayConverter.php b/Neos.Flow/Classes/Property/TypeConverter/TypedArrayConverter.php index f183ca93cf..5550c4b281 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/TypedArrayConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/TypedArrayConverter.php @@ -56,11 +56,11 @@ public function canConvertFrom($source, $targetType) } /** - * @param array $source An array of objects/simple types + * @param array $source An array of objects/simple types * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration - * @return array + * @return array * @api */ public function convertFrom($source, $targetType, array $convertedChildProperties = [], ?PropertyMappingConfigurationInterface $configuration = null) @@ -72,7 +72,7 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie * Returns the source, if it is an array, otherwise an empty array. * * @param mixed $source - * @return array + * @return array */ public function getSourceChildPropertiesToBeConverted($source) { @@ -85,7 +85,7 @@ public function getSourceChildPropertiesToBeConverted($source) * @param string $targetType * @param string $propertyName * @param PropertyMappingConfigurationInterface $configuration - * @return string + * @return ?string */ public function getTypeOfChildProperty($targetType, $propertyName, PropertyMappingConfigurationInterface $configuration) { diff --git a/Neos.Flow/Classes/Property/TypeConverter/UriTypeConverter.php b/Neos.Flow/Classes/Property/TypeConverter/UriTypeConverter.php index f8556886df..3a6299e3c2 100644 --- a/Neos.Flow/Classes/Property/TypeConverter/UriTypeConverter.php +++ b/Neos.Flow/Classes/Property/TypeConverter/UriTypeConverter.php @@ -46,7 +46,7 @@ class UriTypeConverter extends AbstractTypeConverter * * @param string $source The URI to be converted * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return Uri|Error if the input format is not supported or could not be converted for other reasons */ diff --git a/Neos.Flow/Classes/Property/TypeConverterInterface.php b/Neos.Flow/Classes/Property/TypeConverterInterface.php index d9bb5bbc8e..47c0e0b561 100644 --- a/Neos.Flow/Classes/Property/TypeConverterInterface.php +++ b/Neos.Flow/Classes/Property/TypeConverterInterface.php @@ -104,7 +104,7 @@ public function getTypeOfChildProperty($targetType, $propertyName, PropertyMappi * * @param mixed $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return mixed|Error the target type, or an error object if a user-error occurred * @throws Exception\TypeConverterException thrown in case a developer error occurred diff --git a/Neos.Flow/Classes/Reflection/ClassReflection.php b/Neos.Flow/Classes/Reflection/ClassReflection.php index 34664fab6c..6ecc334ca4 100644 --- a/Neos.Flow/Classes/Reflection/ClassReflection.php +++ b/Neos.Flow/Classes/Reflection/ClassReflection.php @@ -16,6 +16,7 @@ /** * Extended version of the ReflectionClass * + * @extends \ReflectionClass * @Flow\Proxy(false) */ class ClassReflection extends \ReflectionClass @@ -39,7 +40,7 @@ static function ($className) use ($classNameOrObject) { } /** - * @var DocCommentParser Holds an instance of the doc comment parser for this class + * @var ?DocCommentParser Holds an instance of the doc comment parser for this class */ protected $docCommentParser; @@ -48,12 +49,14 @@ static function ($className) use ($classNameOrObject) { * that MethodReflection objects are returned instead of the * original ReflectionMethod instances. * - * @return MethodReflection Method reflection object of the constructor method + * @return ?MethodReflection Method reflection object of the constructor method */ - public function getConstructor(): MethodReflection + public function getConstructor(): ?MethodReflection { $parentConstructor = parent::getConstructor(); - return (!is_object($parentConstructor)) ? $parentConstructor : new MethodReflection($this->getName(), $parentConstructor->getName()); + return $parentConstructor === null + ? $parentConstructor + : new MethodReflection($this->getName(), $parentConstructor->getName()); } /** @@ -165,7 +168,7 @@ public function isTaggedWith($tag) /** * Returns an array of tags and their values * - * @return array Tags and values + * @return array> Tags and values */ public function getTagsValues() { @@ -175,7 +178,7 @@ public function getTagsValues() /** * Returns the values of the specified tag * @param string $tag - * @return array Values of the given tag + * @return array Values of the given tag */ public function getTagValues($tag) { @@ -205,7 +208,7 @@ public function newInstanceWithoutConstructor(): object { $instance = parent::newInstanceWithoutConstructor(); - if (method_exists($instance, '__wakeup') && is_callable([$instance, '__wakeup'])) { + if (method_exists($instance, '__wakeup')) { $instance->__wakeup(); } @@ -221,8 +224,11 @@ public function newInstanceWithoutConstructor(): object protected function getDocCommentParser() { if (!is_object($this->docCommentParser)) { - $this->docCommentParser = new DocCommentParser; - $this->docCommentParser->parseDocComment($this->getDocComment()); + $this->docCommentParser = new DocCommentParser(); + $docComment = $this->getDocComment(); + if ($docComment) { + $this->docCommentParser->parseDocComment($docComment); + } } return $this->docCommentParser; } diff --git a/Neos.Flow/Classes/Reflection/ClassSchema.php b/Neos.Flow/Classes/Reflection/ClassSchema.php index df24913f7a..98bd7312aa 100644 --- a/Neos.Flow/Classes/Reflection/ClassSchema.php +++ b/Neos.Flow/Classes/Reflection/ClassSchema.php @@ -11,6 +11,7 @@ * source code. */ +use Neos\Flow\Persistence\RepositoryInterface; use Neos\Utility\Exception\InvalidTypeException; use Neos\Utility\TypeHandling; @@ -47,21 +48,27 @@ class ClassSchema protected $lazyLoadable = false; /** - * @var string|null + * @var class-string|null */ protected $repositoryClassName; /** * Properties of the class which need to be persisted * - * @var array + * @var array */ protected $properties = []; /** * The properties forming the identity of an object * - * @var array + * @var array */ protected $identityProperties = []; @@ -137,7 +144,13 @@ public function addProperty($name, $type, $lazy = false, $transient = false) * hasProperty($propertyName) before! * * @param string $propertyName - * @return array + * @return array{ + * type: string, + * elementType: ?string, + * nullable: bool, + * lazy: bool, + * transient: bool, + * } */ public function getProperty($propertyName) { @@ -147,7 +160,13 @@ public function getProperty($propertyName) /** * Returns all properties defined in this schema * - * @return array + * @return array */ public function getProperties() { @@ -198,7 +217,7 @@ public function getModelType() /** * Set the class name of the repository managing an entity. * - * @param string $repositoryClassName + * @param ?class-string $repositoryClassName * @return void * @throws Exception\ClassSchemaConstraintViolationException */ @@ -211,7 +230,7 @@ public function setRepositoryClassName($repositoryClassName) } /** - * @return string + * @return ?class-string */ public function getRepositoryClassName() { @@ -300,7 +319,7 @@ public function markAsIdentityProperty($propertyName) /** * Gets the properties (names and types) forming the identity of an object. * - * @return array + * @return array * @see markAsIdentityProperty() */ public function getIdentityProperties() diff --git a/Neos.Flow/Classes/Reflection/DocCommentParser.php b/Neos.Flow/Classes/Reflection/DocCommentParser.php index 514a976456..3eca9e1db4 100644 --- a/Neos.Flow/Classes/Reflection/DocCommentParser.php +++ b/Neos.Flow/Classes/Reflection/DocCommentParser.php @@ -28,7 +28,7 @@ class DocCommentParser /** * An array of tag names and their values (multiple values are possible) - * @var array + * @var array> */ protected $tags = []; @@ -47,9 +47,9 @@ public function parseDocComment($docComment) $lines = explode(chr(10), $docComment); foreach ($lines as $line) { - $line = trim(preg_replace('/\*\/$/', '', $line)); + $line = trim(preg_replace('/\*\/$/', '', $line) ?: ''); if ($line !== '' && strpos($line, '* @') !== false) { - $this->parseTag(substr($line, strpos($line, '@'))); + $this->parseTag(substr($line, strpos($line, '@') ?: 0)); } elseif (count($this->tags) === 0) { $this->description .= preg_replace('/\s*\\/?[\\\\*]*\s?(.*)$/', '$1', $line) . chr(10); } @@ -60,7 +60,7 @@ public function parseDocComment($docComment) /** * Returns the tags which have been previously parsed * - * @return array Array of tag names and their (multiple) values + * @return array> Array of tag names and their (multiple) values */ public function getTagsValues() { @@ -73,7 +73,7 @@ public function getTagsValues() * available. * * @param string $tagName The tag name to retrieve the values for - * @return array The tag's values + * @return array The tag's values * @throws Exception */ public function getTagValues($tagName) @@ -116,7 +116,7 @@ protected function parseTag($line) { $tagAndValue = []; if (preg_match('/@[A-Za-z0-9\\\\]+\\\\([A-Za-z0-9]+)(?:\\((.*)\\))?$/', $line, $tagAndValue) === 0) { - $tagAndValue = preg_split('/\s/', $line, 2); + $tagAndValue = preg_split('/\s/', $line, 2) ?: []; } else { array_shift($tagAndValue); } diff --git a/Neos.Flow/Classes/Reflection/MethodReflection.php b/Neos.Flow/Classes/Reflection/MethodReflection.php index b5e8a0e1e7..37b4317881 100644 --- a/Neos.Flow/Classes/Reflection/MethodReflection.php +++ b/Neos.Flow/Classes/Reflection/MethodReflection.php @@ -21,7 +21,7 @@ class MethodReflection extends \ReflectionMethod { /** - * @var DocCommentParser An instance of the doc comment parser + * @var ?DocCommentParser An instance of the doc comment parser */ protected $docCommentParser; @@ -66,7 +66,7 @@ public function isTaggedWith($tag) /** * Returns an array of tags and their values * - * @return array Tags and values + * @return array> Tags and values */ public function getTagsValues() { @@ -77,7 +77,7 @@ public function getTagsValues() * Returns the values of the specified tag * * @param string $tag Tag name to check for - * @return array Values of the given tag + * @return array Values of the given tag */ public function getTagValues($tag) { @@ -99,9 +99,6 @@ public function getDescription() */ public function getDeclaredReturnType() { - if (!is_callable([$this, 'getReturnType'])) { - return null; - } $type = $this->getReturnType(); return $type !== null ? ltrim((string)$type, '?') : null; } @@ -111,9 +108,6 @@ public function getDeclaredReturnType() */ public function isDeclaredReturnTypeNullable() { - if (!is_callable([$this, 'getReturnType'])) { - return false; - } $type = $this->getReturnType(); return $type !== null && $type->allowsNull(); } @@ -127,8 +121,11 @@ public function isDeclaredReturnTypeNullable() protected function getDocCommentParser() { if (!is_object($this->docCommentParser)) { - $this->docCommentParser = new DocCommentParser; - $this->docCommentParser->parseDocComment($this->getDocComment()); + $this->docCommentParser = new DocCommentParser(); + $docComment = $this->getDocComment(); + if ($docComment) { + $this->docCommentParser->parseDocComment($docComment); + } } return $this->docCommentParser; } diff --git a/Neos.Flow/Classes/Reflection/ParameterReflection.php b/Neos.Flow/Classes/Reflection/ParameterReflection.php index 94c560aac0..11d4831436 100644 --- a/Neos.Flow/Classes/Reflection/ParameterReflection.php +++ b/Neos.Flow/Classes/Reflection/ParameterReflection.php @@ -28,11 +28,12 @@ class ParameterReflection extends \ReflectionParameter /** * Returns the declaring class * - * @return ClassReflection The declaring class + * @return ?ClassReflection The declaring class */ - public function getDeclaringClass(): ClassReflection + public function getDeclaringClass(): ?ClassReflection { - return new ClassReflection(parent::getDeclaringClass()->getName()); + $reflectionClass = parent::getDeclaringClass(); + return $reflectionClass ? new ClassReflection($reflectionClass->getName()) : null; } /** diff --git a/Neos.Flow/Classes/Reflection/PropertyReflection.php b/Neos.Flow/Classes/Reflection/PropertyReflection.php index 1b59ba4c4e..4afc83c6f8 100644 --- a/Neos.Flow/Classes/Reflection/PropertyReflection.php +++ b/Neos.Flow/Classes/Reflection/PropertyReflection.php @@ -21,7 +21,7 @@ class PropertyReflection extends \ReflectionProperty { /** - * @var DocCommentParser An instance of the doc comment parser + * @var ?DocCommentParser An instance of the doc comment parser */ protected $docCommentParser; @@ -58,7 +58,7 @@ public function getDeclaringClass(): ClassReflection /** * Returns an array of tags and their values * - * @return array Tags and values + * @return array> Tags and values */ public function getTagsValues() { @@ -69,7 +69,7 @@ public function getTagsValues() * Returns the values of the specified tag * * @param string $tag - * @return array Values of the given tag + * @return array Values of the given tag */ public function getTagValues($tag) { @@ -137,8 +137,11 @@ public function setValue($object = null, $value = null): void protected function getDocCommentParser() { if (!is_object($this->docCommentParser)) { - $this->docCommentParser = new DocCommentParser; - $this->docCommentParser->parseDocComment($this->getDocComment()); + $this->docCommentParser = new DocCommentParser(); + $docComment = $this->getDocComment(); + if ($docComment) { + $this->docCommentParser->parseDocComment($docComment); + } } return $this->docCommentParser; } diff --git a/Neos.Flow/Classes/Reflection/ReflectionService.php b/Neos.Flow/Classes/Reflection/ReflectionService.php index 5c720b5db0..f9d457c48f 100644 --- a/Neos.Flow/Classes/Reflection/ReflectionService.php +++ b/Neos.Flow/Classes/Reflection/ReflectionService.php @@ -92,6 +92,9 @@ class ReflectionService protected const DATA_PARAMETER_ANNOTATIONS = 25; protected Reader $annotationReader; + /** + * @var array> class names per package (key) + */ protected array $availableClassNames = []; protected VariableFrontend $reflectionDataRuntimeCache; protected VariableFrontend $classSchemataRuntimeCache; @@ -107,40 +110,87 @@ class ReflectionService /** * a cache which stores the use statements reflected for a particular class * (only relevant for un-expanded "var" and "param" annotations) + * + * @var array> */ protected array $useStatementsForClassCache; + /** + * @var array + */ protected array $settings = []; /** * Array of annotation classnames and the names of classes which are annotated with them + * + * @var array> */ protected array $annotatedClasses = []; /** * Array of method annotations and the classes and methods which are annotated with them + * + * @var array>> */ protected array $classesByMethodAnnotations = []; /** * Schemata of all classes which can be persisted * - * @var array + * @var array */ protected array $classSchemata = []; /** * An array of class names which are currently being forgotten by forgetClass(). Acts as a safeguard against infinite loops. + * + * @var array */ protected array $classesCurrentlyBeingForgotten = []; /** * Array with reflection information indexed by class name + * + * @var array, + * 3?: array, + * 5?: array, + * 6?: true, + * 7?: true, + * 8?: array> + * }>, + * 25: ?string + * }>, + * 9?: array>, + * 15?: array>, + * 24?: int, + * 26?: string, + * 28?: true, + * }>, + * 27?: true, + * }|false> */ protected array $classReflectionData = []; /** * Array with updated reflection information (e.g. in Development context after classes have changed) + * + * @var array */ protected array $updatedReflectionData = []; @@ -148,6 +198,8 @@ class ReflectionService /** * A runtime cache for reflected method annotations to speed up repeating checks. + * + * @var array>> */ protected array $methodAnnotationsRuntimeCache = []; @@ -162,6 +214,9 @@ public function setClassSchemataRuntimeCache(VariableFrontend $cache): void $this->classSchemataRuntimeCache = $cache; } + /** + * @param array $settings + */ public function injectSettings(array $settings): void { $this->settings = $settings['reflection']; @@ -211,7 +266,7 @@ protected function initialize(): void * This method is called by the Compile Time Object Manager which also determines * the list of classes to consider for reflection. * - * @param array $availableClassNames + * @param array> $availableClassNames * @throws ClassLoadingForReflectionFailedException * @throws ClassSchemaConstraintViolationException * @throws Exception @@ -251,6 +306,7 @@ public function isClassReflected(string $className): bool /** * Returns the names of all classes known to this reflection service. * + * @return array * @api */ public function getAllClassNames(): array @@ -268,19 +324,20 @@ public function getAllClassNames(): array * implementation was found in the package defining the interface, false is returned. * * @param class-string $interfaceName - * @return string|bool + * @return class-string|false * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException * @throws \ReflectionException * @api */ - public function getDefaultImplementationClassNameForInterface(string $interfaceName): string|bool + public function getDefaultImplementationClassNameForInterface(string $interfaceName): string|false { if (interface_exists($interfaceName) === false) { throw new \InvalidArgumentException('"' . $interfaceName . '" does not exist or is not the name of an interface.', 1238769559); } $interfaceName = $this->prepareClassReflectionForUsage($interfaceName); + /** @var array $classNamesFound */ $classNamesFound = array_keys($this->classReflectionData[$interfaceName][self::DATA_INTERFACE_IMPLEMENTATIONS] ?? []); if (count($classNamesFound) === 1) { return $classNamesFound[0]; @@ -318,6 +375,7 @@ public function getAllImplementationClassNamesForInterface(string $interfaceName } $interfaceName = $this->prepareClassReflectionForUsage($interfaceName); + /** @phpstan-ignore return.type (classic subtype dilemma; don't know how to solve it yet) */ return array_keys($this->classReflectionData[$interfaceName][self::DATA_INTERFACE_IMPLEMENTATIONS] ?? []); } @@ -325,8 +383,9 @@ public function getAllImplementationClassNamesForInterface(string $interfaceName * Searches for and returns all names of classes inheriting the specified class. * If no class inheriting the given class was found, an empty array is returned. * - * @param class-string $className - * @return array + * @template T of object + * @param class-string $className + * @return array> * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException * @throws \ReflectionException @@ -338,12 +397,16 @@ public function getAllSubClassNamesForClass(string $className): array throw new \InvalidArgumentException('"' . $className . '" does not exist or is not the name of a class.', 1257168042); } $className = $this->prepareClassReflectionForUsage($className); + /** @phpstan-ignore return.type (classic subtype dilemma; don't know how to solve it yet) */ return array_keys($this->classReflectionData[$className][self::DATA_CLASS_SUBCLASSES] ?? []); } /** * Searches for and returns all names of classes which are tagged by the specified * annotation. If no classes were found, an empty array is returned. + * + * @param class-string $annotationClassName + * @return array */ public function getClassNamesByAnnotation(string $annotationClassName): array { @@ -358,6 +421,8 @@ public function getClassNamesByAnnotation(string $annotationClassName): array /** * Tells if the specified class has the given annotation * + * @param class-string $className + * @param class-string $annotationClassName * @api */ public function isClassAnnotatedWith(string $className, string $annotationClassName): bool @@ -375,9 +440,10 @@ public function isClassAnnotatedWith(string $className, string $annotationClassN /** * Returns the specified class annotations or an empty array * + * @template T * @param class-string $className - * @param null|class-string $annotationClassName - * @return array + * @param null|class-string $annotationClassName + * @return ($annotationClassName is null ? array : array) * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException * @throws \ReflectionException @@ -410,9 +476,10 @@ public function getClassAnnotations(string $className, string|null $annotationCl * If multiple annotations are set on the target you will * get the first instance of them. * + * @template T of object * @param class-string $className - * @param class-string $annotationClassName - * @return object|null + * @param class-string $annotationClassName + * @return T|null * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException * @throws \ReflectionException @@ -424,12 +491,15 @@ public function getClassAnnotation(string $className, string $annotationClassNam } $annotations = $this->getClassAnnotations($className, $annotationClassName); - return $annotations === [] ? null : reset($annotations); + return $annotations[0] ?? null; } /** * Tells if the specified class implements the given interface * + * @todo can't this be replaces with is_subclass_of() ? + * @param class-string $className + * @param class-string $interfaceName * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException * @throws \ReflectionException @@ -476,7 +546,7 @@ public function isClassFinal(string $className): bool /** * Tells if the specified class is readonly or not * - * @param string $className Name of the class to analyze + * @param class-string $className Name of the class to analyze * @return bool true if the class is readonly, otherwise false * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException @@ -492,6 +562,7 @@ public function isClassReadonly(string $className): bool /** * Tells if the class is unconfigurable or not * + * @param class-string $className * @api */ public function isClassUnconfigurable(string $className): bool @@ -505,6 +576,7 @@ public function isClassUnconfigurable(string $className): bool * Returns all class names of classes containing at least one method annotated * with the given annotation class * + * @return array * @api */ public function getClassesContainingMethodsAnnotatedWith(string $annotationClassName): array @@ -519,6 +591,7 @@ public function getClassesContainingMethodsAnnotatedWith(string $annotationClass /** * Returns all names of methods of the given class that are annotated with the given annotation class * + * @return array * @api */ public function getMethodsAnnotatedWith(string $className, string $annotationClassName): array @@ -532,7 +605,7 @@ public function getMethodsAnnotatedWith(string $className, string $annotationCla /** * Tells if the specified method is final or not * - * @param string $className + * @param class-string $className * @param string $methodName * @return bool * @throws ClassLoadingForReflectionFailedException @@ -603,6 +676,7 @@ public function isMethodPrivate(string $className, string $methodName): bool /** * Tells if the specified method is tagged with the given tag * + * @param class-string $className * @throws \ReflectionException * @api */ @@ -643,9 +717,10 @@ public function isMethodAnnotatedWith(string $className, string $methodName, str /** * Returns the specified method annotations or an empty array * + * @template T * @param class-string $className - * @param class-string|null $annotationClassName - * @return array + * @param class-string|null $annotationClassName + * @return ($annotationClassName is null ? array : array) * * @throws \ReflectionException * @api @@ -692,6 +767,11 @@ public function getMethodAnnotations(string $className, string $methodName, stri * If multiple annotations are set on the target you will * get the first instance of them. * + * @template T of object + * @param class-string $className + * @param class-string $annotationClassName + * @return ?T + * * @throws \ReflectionException */ public function getMethodAnnotation(string $className, string $methodName, string $annotationClassName): ?object @@ -740,6 +820,8 @@ public function hasMethod(string $className, string $methodName): bool * * @throws \ReflectionException * @deprecated since 8.4 + * @param class-string $className + * @return array> */ public function getMethodTagsValues(string $className, string $methodName): array { @@ -756,7 +838,18 @@ public function getMethodTagsValues(string $className, string $methodName): arra * additional information about the parameter position, type hint etc. * * @param class-string $className - * @return array An array of parameter names and additional information or an empty array of no parameters were found + * @return array>, + * }> An array of parameter names and additional information or an empty array of no parameters were found * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException * @throws \ReflectionException @@ -819,7 +912,7 @@ public function getPropertyNamesByTag(string $className, string $tag): array * * @param class-string $className * @param string $propertyName - * @return array + * @return array> * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException * @throws \ReflectionException @@ -837,7 +930,7 @@ public function getPropertyTagsValues(string $className, string $propertyName): * @param class-string $className * @param string $propertyName * @param string $tag - * @return array + * @return array * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException * @throws \ReflectionException @@ -884,6 +977,7 @@ public function isPropertyPrivate(string $className, string $propertyName): bool /** * Tells if the specified property is promoted * + * @param class-string $className * @api */ public function isPropertyPromoted(string $className, string $propertyName): bool @@ -958,10 +1052,11 @@ public function getPropertyNamesByAnnotation(string $className, string $annotati /** * Returns the specified property annotations or an empty array * + * @template Annotation of object * @param class-string $className * @param string $propertyName - * @param class-string|null $annotationClassName - * @return array + * @param class-string|null $annotationClassName + * @return ($annotationClassName is null ? array> : array) * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException * @throws \ReflectionException @@ -978,6 +1073,9 @@ public function getPropertyAnnotations(string $className, string $propertyName, return $this->classReflectionData[$className][self::DATA_CLASS_PROPERTIES][$propertyName][self::DATA_PROPERTY_ANNOTATIONS]; } + /** @phpstan-ignore return.type + * (in case of an annotation class name, this is really the annotation instance; The array iss just mixed with arbitrary-string-indexed other objects) + */ return $this->classReflectionData[$className][self::DATA_CLASS_PROPERTIES][$propertyName][self::DATA_PROPERTY_ANNOTATIONS][$annotationClassName] ?? []; } @@ -987,10 +1085,11 @@ public function getPropertyAnnotations(string $className, string $propertyName, * If multiple annotations are set on the target you will * get the first instance of them. * - * @param string $className + * @template Annotation of object + * @param class-string $className * @param string $propertyName - * @param string $annotationClassName - * @return object|null + * @param class-string $annotationClassName + * @return Annotation|null * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException * @throws \ReflectionException @@ -1001,7 +1100,7 @@ public function getPropertyAnnotation(string $className, string $propertyName, s $this->initialize(); } $annotations = $this->getPropertyAnnotations($className, $propertyName, $annotationClassName); - return $annotations === [] ? null : reset($annotations); + return reset($annotations) ?: null; } /** @@ -1017,8 +1116,8 @@ public function getClassSchema(string|object $classNameOrObject): ?ClassSchema } $className = $classNameOrObject; - if (is_object($classNameOrObject)) { - $className = TypeHandling::getTypeForValue($classNameOrObject); + if (is_object($className)) { + $className = TypeHandling::getTypeForValue($className); } $className = $this->cleanClassName($className); @@ -1034,7 +1133,7 @@ public function getClassSchema(string|object $classNameOrObject): ?ClassSchema * Initializes the ReflectionService, cleans the given class name and finally reflects the class if necessary. * * @param class-string $className - * @return string + * @return class-string * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException * @throws \ReflectionException @@ -1147,12 +1246,15 @@ protected function reflectClass(string $className): void } if ($class->isAbstract() || $class->isInterface()) { + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$className][self::DATA_CLASS_ABSTRACT] = true; } if ($class->isFinal()) { + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$className][self::DATA_CLASS_FINAL] = true; } if ($class->isReadOnly()) { + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$className][self::DATA_CLASS_READONLY] = true; } @@ -1167,6 +1269,7 @@ protected function reflectClass(string $className): void foreach ($this->annotationReader->getClassAnnotations($class) as $annotation) { $annotationClassName = get_class($annotation); $this->annotatedClasses[$annotationClassName][$className] = true; + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$className][self::DATA_CLASS_ANNOTATIONS][] = $annotation; } @@ -1176,6 +1279,7 @@ protected function reflectClass(string $className): void continue; } $this->annotatedClasses[$annotationClassName][$className] = true; + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$className][self::DATA_CLASS_ANNOTATIONS][] = $attribute->newInstance(); } @@ -1194,6 +1298,7 @@ protected function reflectClass(string $className): void } /** + * @param class-string $className * @return int visibility */ public function reflectClassProperty(string $className, PropertyReflection $property): int @@ -1203,6 +1308,7 @@ public function reflectClassProperty(string $className, PropertyReflection $prop } $propertyName = $property->getName(); + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$className][self::DATA_CLASS_PROPERTIES][$propertyName] = []; if ($property->hasType()) { $this->classReflectionData[$className][self::DATA_CLASS_PROPERTIES][$propertyName][self::DATA_PROPERTY_TYPE] = trim((string)$property->getType(), '?'); @@ -1242,6 +1348,10 @@ public function reflectClassProperty(string $className, PropertyReflection $prop return $visibility; } + /** + * @param array $tagValues + * @return array|null + */ protected function reflectPropertyTag(string $className, PropertyReflection $property, string $tagName, array $tagValues): ?array { if ($this->isTagIgnored($tagName)) { @@ -1263,20 +1373,25 @@ protected function reflectPropertyTag(string $className, PropertyReflection $pro } /** + * @param class-string $className * @throws InvalidClassException * @throws ClassLoadingForReflectionFailedException * @throws \ReflectionException */ protected function addParentClass(string $className, ClassReflection $parentClass): void { + /** @var class-string $parentClassName */ $parentClassName = $parentClass->getName(); if (!$this->isClassReflected($parentClassName)) { $this->loadOrReflectClassIfNecessary($parentClassName); } + + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$parentClassName][self::DATA_CLASS_SUBCLASSES][$className] = true; } /** + * @param class-string $className * @throws ClassLoadingForReflectionFailedException * @throws InvalidClassException * @throws \ReflectionException @@ -1292,6 +1407,7 @@ protected function addImplementedInterface(string $className, ClassReflection $i $this->loadOrReflectClassIfNecessary($interfaceName); } + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$interfaceName][self::DATA_INTERFACE_IMPLEMENTATIONS][$className] = true; } @@ -1304,11 +1420,14 @@ protected function reflectClassMethod(string $className, MethodReflection $metho { $methodName = $method->getName(); if ($method->isFinal()) { + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$className][self::DATA_CLASS_METHODS][$methodName][self::DATA_METHOD_FINAL] = true; } if ($method->isStatic()) { + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$className][self::DATA_CLASS_METHODS][$methodName][self::DATA_METHOD_STATIC] = true; } + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$className][self::DATA_CLASS_METHODS][$methodName][self::DATA_METHOD_VISIBILITY] = $this->extractVisibility($method); foreach ($this->getMethodAnnotations($className, $methodName) as $methodAnnotation) { @@ -1340,6 +1459,7 @@ protected function reflectClassMethod(string $className, MethodReflection $metho } } } + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$className][self::DATA_CLASS_METHODS][$methodName][self::DATA_METHOD_DECLARED_RETURN_TYPE] = $returnType; foreach ($method->getParameters() as $parameter) { @@ -1364,6 +1484,7 @@ protected function reflectClassMethodParameter(string $className, MethodReflecti $methodName = $method->getName(); $paramAnnotations = $method->isTaggedWith('param') ? $method->getTagValues('param') : []; + /** @phpstan-ignore offsetAccess.nonOffsetAccessible (is not false, todo simplify structure to always be an array) */ $this->classReflectionData[$className][self::DATA_CLASS_METHODS][$methodName][self::DATA_METHOD_PARAMETERS][$parameter->getName()] = $this->convertParameterReflectionToArray($parameter, $method); if (!isset($this->settings['logIncorrectDocCommentHints']) || $this->settings['logIncorrectDocCommentHints'] !== true) { return; @@ -1381,7 +1502,7 @@ protected function reflectClassMethodParameter(string $className, MethodReflecti } if ( - isset($this->classReflectionData[$className][self::DATA_CLASS_METHODS][$methodName][self::DATA_METHOD_PARAMETERS][$parameter->getName()][self::DATA_PARAMETER_TYPE]) && + isset($this->classReflectionData[$className][self::DATA_CLASS_METHODS][$methodName][self::DATA_METHOD_PARAMETERS][$parameter->getName()][self::DATA_PARAMETER_TYPE]) && class_exists($parameterAnnotation[0]) && $this->classReflectionData[$className][self::DATA_CLASS_METHODS][$methodName][self::DATA_METHOD_PARAMETERS][$parameter->getName()][self::DATA_PARAMETER_TYPE] !== $this->cleanClassName($parameterAnnotation[0]) ) { $this->log(' Wrong type in @param for "' . $method->getName() . '::' . $parameter->getName() . '": "' . $parameterAnnotation[0] . '"', LogLevel::DEBUG); @@ -1447,6 +1568,7 @@ protected function expandType(ClassReflection $class, string $type): string /** * Finds all parent classes of the given class * + * @param array $parentClasses * @return array */ protected function getParentClasses(ClassReflection $class, array $parentClasses = []): array @@ -1514,7 +1636,11 @@ protected function buildClassSchema(string $className): ClassSchema } $possibleRepositoryClassName = str_replace('\\Model\\', '\\Repository\\', $className) . 'Repository'; - if ($this->isClassReflected($possibleRepositoryClassName) === true) { + if ( + class_exists($possibleRepositoryClassName) + && is_subclass_of($possibleRepositoryClassName, RepositoryInterface::class) + && $this->isClassReflected($possibleRepositoryClassName) === true + ) { $classSchema->setRepositoryClassName($possibleRepositoryClassName); } @@ -1540,7 +1666,6 @@ protected function addPropertiesToClassSchema(ClassSchema $classSchema): void $className = $classSchema->getClassName(); $skipArtificialIdentity = false; - /* @var $valueObjectAnnotation Flow\ValueObject */ $valueObjectAnnotation = $this->getClassAnnotation($className, Flow\ValueObject::class); if ($valueObjectAnnotation !== null && $valueObjectAnnotation->embedded === true) { $skipArtificialIdentity = true; @@ -1594,6 +1719,9 @@ protected function evaluateClassPropertyAnnotationsForSchema(ClassSchema $classS } $declaredType = strtok(trim(current($varTagValues), " \n\t"), " \n\t"); + if ($declaredType === false) { + throw new \Exception('Invalid type for ' . $className . '::$' . $propertyName, 1744046737); + } if ($this->isPropertyAnnotatedWith($className, $propertyName, ORM\Id::class)) { $skipArtificialIdentity = true; @@ -1644,7 +1772,7 @@ protected function completeRepositoryAssignments(): void } foreach ($this->classSchemata as $classSchema) { - if ($classSchema instanceof ClassSchema && class_exists($classSchema->getClassName()) && $classSchema->isAggregateRoot()) { + if (class_exists($classSchema->getClassName()) && $classSchema->isAggregateRoot()) { $this->makeChildClassesAggregateRoot($classSchema); } } @@ -1684,7 +1812,7 @@ protected function makeChildClassesAggregateRoot(ClassSchema $classSchema): void protected function ensureAggregateRootInheritanceChainConsistency(): void { foreach ($this->classSchemata as $className => $classSchema) { - if (!class_exists($className) || ($classSchema instanceof ClassSchema && $classSchema->isAggregateRoot() === false)) { + if (!class_exists($className) || ($classSchema->isAggregateRoot() === false)) { continue; } @@ -1726,8 +1854,30 @@ protected function checkValueObjectRequirements(string $className): void * Converts the internal, optimized data structure of parameter information into * a human-friendly array with speaking indexes. * - * @param array $parametersInformation Raw, internal parameter information - * @return array Developer friendly version + * @param array>, + * }> $parametersInformation Raw, internal parameter information + * @return array>, + * }> Developer friendly version */ protected function convertParameterDataToArray(array $parametersInformation): array { @@ -1736,7 +1886,7 @@ protected function convertParameterDataToArray(array $parametersInformation): ar $parameters[$parameterName] = [ 'position' => $parameterData[self::DATA_PARAMETER_POSITION], 'optional' => isset($parameterData[self::DATA_PARAMETER_OPTIONAL]), - 'type' => $parameterData[self::DATA_PARAMETER_TYPE], + 'type' => $parameterData[self::DATA_PARAMETER_TYPE] ?? null, 'class' => $parameterData[self::DATA_PARAMETER_CLASS] ?? null, 'array' => isset($parameterData[self::DATA_PARAMETER_ARRAY]), 'byReference' => isset($parameterData[self::DATA_PARAMETER_BY_REFERENCE]), @@ -1753,7 +1903,18 @@ protected function convertParameterDataToArray(array $parametersInformation): ar /** * Converts the given parameter reflection into an information array * - * @return array Parameter information array + * @return array{ + * 16: int, + * 17?: true, + * 18?: string, + * 19?: true, + * 20?: string, + * 21?: true, + * 22?: mixed, + * 23?: true, + * 24?: true, + * 25?: array> + * } Parameter information array */ protected function convertParameterReflectionToArray(ParameterReflection $parameter, MethodReflection $method): array { @@ -1794,7 +1955,7 @@ protected function convertParameterReflectionToArray(ParameterReflection $parame break; } } - if (!isset($parameterInformation[self::DATA_PARAMETER_TYPE]) && $parameterType !== null) { + if (!isset($parameterInformation[self::DATA_PARAMETER_TYPE]) && $parameterType !== null && class_exists($parameterType)) { $parameterInformation[self::DATA_PARAMETER_TYPE] = $this->cleanClassName($parameterType); } elseif (!isset($parameterInformation[self::DATA_PARAMETER_TYPE])) { $parameterInformation[self::DATA_PARAMETER_TYPE] = 'mixed'; @@ -1820,7 +1981,7 @@ protected function convertParameterReflectionToArray(ParameterReflection $parame protected function forgetChangedClasses(): void { foreach ($this->classReflectionData as $className => $_) { - if (is_string($className) && !$this->reflectionDataRuntimeCache->has($this->produceCacheIdentifierFromClassName($className))) { + if (!$this->reflectionDataRuntimeCache->has($this->produceCacheIdentifierFromClassName($className))) { $this->forgetClass($className); } } @@ -1953,6 +2114,8 @@ public function saveToCache(): void /** * Clean a given class name from possibly prefixed backslash + * @param class-string $className + * @return class-string */ protected function cleanClassName(string $className): string { @@ -1969,6 +2132,7 @@ protected function produceCacheIdentifierFromClassName(string $className): strin /** * Writes the given message along with the additional information into the log. + * @param array $additionalData */ protected function log(string $message, string $severity = LogLevel::INFO, array $additionalData = []): void { @@ -1992,7 +2156,7 @@ function (\ReflectionNamedType|\ReflectionIntersectionType $type) { $parameterType->getTypes() )), $parameterType instanceof \ReflectionIntersectionType => implode('&', array_map( - function (\ReflectionNamedType $type) { + function (\ReflectionType $type) { return $this->renderParameterType($type); }, $parameterType->getTypes() diff --git a/Neos.Flow/Classes/Reflection/ReflectionServiceFactory.php b/Neos.Flow/Classes/Reflection/ReflectionServiceFactory.php index e3e5ed1429..61ef500d0f 100644 --- a/Neos.Flow/Classes/Reflection/ReflectionServiceFactory.php +++ b/Neos.Flow/Classes/Reflection/ReflectionServiceFactory.php @@ -11,6 +11,7 @@ * source code. */ +use Neos\Cache\Frontend\VariableFrontend; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cache\CacheManager; use Neos\Flow\Configuration\ConfigurationManager; @@ -62,8 +63,16 @@ public function create(): ReflectionService $reflectionService = new ReflectionService(); $reflectionService->injectLogger($this->bootstrap->getEarlyInstance(PsrLoggerFactoryInterface::class)->get('systemLogger')); $reflectionService->injectSettings($settings); - $reflectionService->setReflectionDataRuntimeCache($cacheManager->getCache('Flow_Reflection_RuntimeData')); - $reflectionService->setClassSchemataRuntimeCache($cacheManager->getCache('Flow_Reflection_RuntimeClassSchemata')); + $runtimeDataCache = $cacheManager->getCache('Flow_Reflection_RuntimeData'); + if (!$runtimeDataCache instanceof VariableFrontend) { + throw new \Exception('Cache Flow_Reflection_RuntimeData must have a VariableFrontend', 1743972055); + } + $reflectionService->setReflectionDataRuntimeCache($runtimeDataCache); + $runtimeClassSchemataCache = $cacheManager->getCache('Flow_Reflection_RuntimeClassSchemata'); + if (!$runtimeClassSchemataCache instanceof VariableFrontend) { + throw new \Exception('Cache Flow_Reflection_RuntimeClassSchemata must have a VariableFrontend', 1743972058); + } + $reflectionService->setClassSchemataRuntimeCache($runtimeClassSchemataCache); $this->reflectionService = $reflectionService; return $reflectionService; diff --git a/Neos.Flow/Classes/ResourceManagement/Collection.php b/Neos.Flow/Classes/ResourceManagement/Collection.php index 6b07307bb0..275e85cf4c 100644 --- a/Neos.Flow/Classes/ResourceManagement/Collection.php +++ b/Neos.Flow/Classes/ResourceManagement/Collection.php @@ -40,7 +40,7 @@ class Collection implements CollectionInterface protected $target; /** - * @var array + * @var array */ protected $pathPatterns; @@ -56,7 +56,7 @@ class Collection implements CollectionInterface * @param string $name User-space name of this collection, as specified in the settings * @param StorageInterface $storage The storage for data used in this collection * @param TargetInterface $target The publication target for this collection - * @param array $pathPatterns Glob patterns for paths to consider – only supported by specific storages + * @param array $pathPatterns Glob patterns for paths to consider – only supported by specific storages */ public function __construct($name, StorageInterface $storage, TargetInterface $target, array $pathPatterns) { diff --git a/Neos.Flow/Classes/ResourceManagement/EelHelper/StaticResourceHelper.php b/Neos.Flow/Classes/ResourceManagement/EelHelper/StaticResourceHelper.php index a4f61a2a13..fbaf18510e 100644 --- a/Neos.Flow/Classes/ResourceManagement/EelHelper/StaticResourceHelper.php +++ b/Neos.Flow/Classes/ResourceManagement/EelHelper/StaticResourceHelper.php @@ -73,7 +73,7 @@ protected function getLocalizedResourcePath(string $packageKey, string $pathAndF $resourcePath = sprintf('resource://%s/%s', $packageKey, $pathAndFilename); if ($localize === true) { $localizedResourcePathData = $this->i18nService->getLocalizedFilename($resourcePath); - $resourcePath = $localizedResourcePathData[0] ?? $resourcePath; + $resourcePath = $localizedResourcePathData[0]; } return $resourcePath; } diff --git a/Neos.Flow/Classes/ResourceManagement/PersistentResource.php b/Neos.Flow/Classes/ResourceManagement/PersistentResource.php index f431792dde..4e47d9f839 100644 --- a/Neos.Flow/Classes/ResourceManagement/PersistentResource.php +++ b/Neos.Flow/Classes/ResourceManagement/PersistentResource.php @@ -179,7 +179,7 @@ public function setFilename($filename) $this->throwExceptionIfProtected(); $pathInfo = UnicodeFunctions::pathinfo($filename); - $extension = (isset($pathInfo['extension']) ? '.' . strtolower($pathInfo['extension']) : ''); + $extension = '.' . strtolower($pathInfo['extension']); $this->filename = $pathInfo['filename'] . $extension; $this->mediaType = Utility\MediaTypes::getMediaTypeFromFilename($this->filename); } @@ -298,10 +298,10 @@ public function getSha1() * @return void * @api */ - public function setSha1($sha1) + public function setSha1(string $sha1) { $this->throwExceptionIfProtected(); - if (!is_string($sha1) || preg_match('/[A-Fa-f0-9]{40}/', $sha1) !== 1) { + if (preg_match('/[A-Fa-f0-9]{40}/', $sha1) !== 1) { throw new \InvalidArgumentException('Specified invalid hash to setSha1()', 1362564220); } $this->sha1 = strtolower($sha1); @@ -330,9 +330,9 @@ public function createTemporaryLocalCopy() $temporaryPathAndFilename .= '-' . microtime(true); if (function_exists('posix_getpid')) { - $temporaryPathAndFilename .= '-' . str_pad(posix_getpid(), 10); + $temporaryPathAndFilename .= '-' . str_pad((string)posix_getpid(), 10); } else { - $temporaryPathAndFilename .= '-' . (string) getmypid(); + $temporaryPathAndFilename .= '-' . getmypid(); } $temporaryPathAndFilename = trim($temporaryPathAndFilename); @@ -364,7 +364,9 @@ public function postPersist() { if ($this->lifecycleEventsActive) { $collection = $this->resourceManager->getCollection($this->collectionName); - $collection->getTarget()->publishResource($this, $collection); + if ($collection) { + $collection->getTarget()->publishResource($this, $collection); + } } } diff --git a/Neos.Flow/Classes/ResourceManagement/Publishing/MessageCollector.php b/Neos.Flow/Classes/ResourceManagement/Publishing/MessageCollector.php index ffba74d24a..b5b6ecefc0 100644 --- a/Neos.Flow/Classes/ResourceManagement/Publishing/MessageCollector.php +++ b/Neos.Flow/Classes/ResourceManagement/Publishing/MessageCollector.php @@ -35,7 +35,7 @@ class MessageCollector ]; /** - * @var \SplObjectStorage + * @var \SplObjectStorage */ protected $messages; diff --git a/Neos.Flow/Classes/ResourceManagement/ResourceManager.php b/Neos.Flow/Classes/ResourceManagement/ResourceManager.php index 8956aa1770..c513b5bb7a 100644 --- a/Neos.Flow/Classes/ResourceManagement/ResourceManager.php +++ b/Neos.Flow/Classes/ResourceManagement/ResourceManager.php @@ -65,7 +65,7 @@ class ResourceManager protected $persistenceManager; /** - * @var array + * @var array */ protected $settings; @@ -98,7 +98,7 @@ class ResourceManager /** * Injects the settings of this package * - * @param array $settings + * @param array $settings * @return void */ public function injectSettings(array $settings) @@ -156,7 +156,6 @@ public function importResource($source, $collectionName = ResourceManager::DEFAU throw new Exception(sprintf('Tried to import a file into the resource collection "%s" but no such collection exists. Please check your settings and the code which triggered the import.', $collectionName), 1375196643); } - /* @var CollectionInterface $collection */ $collection = $this->collections[$collectionName]; try { @@ -165,15 +164,17 @@ public function importResource($source, $collectionName = ResourceManager::DEFAU ObjectAccess::setProperty($resource, 'Persistence_Object_Identifier', $forcedPersistenceObjectIdentifier, true); } if (!is_resource($source)) { + /** @var string $source */ $pathInfo = UnicodeFunctions::pathinfo($source); + /** @var PersistentResource $resource */ $resource->setFilename($pathInfo['basename']); } } catch (Exception $exception) { throw new Exception(sprintf('Importing a file into the resource collection "%s" failed: %s', $collectionName, $exception->getMessage()), 1375197120, $exception); } - + /** @var PersistentResource $resource */ $this->resourceRepository->add($resource); - $this->logger->debug(sprintf('Successfully imported file "%s" into the resource collection "%s" (storage: %s, a %s. SHA1: %s)', $source, $collectionName, $collection->getStorage()->getName(), get_class($collection), $resource->getSha1())); + $this->logger->debug(sprintf('Successfully imported file into the resource collection "%s" (storage: %s, a %s. SHA1: %s)', $collectionName, $collection->getStorage()->getName(), get_class($collection), $resource->getSha1())); return $resource; } @@ -195,18 +196,14 @@ public function importResource($source, $collectionName = ResourceManager::DEFAU * @throws Exception * @api */ - public function importResourceFromContent($content, $filename, $collectionName = ResourceManager::DEFAULT_PERSISTENT_COLLECTION_NAME, $forcedPersistenceObjectIdentifier = null) + public function importResourceFromContent(string $content, $filename, $collectionName = ResourceManager::DEFAULT_PERSISTENT_COLLECTION_NAME, $forcedPersistenceObjectIdentifier = null) { - if (!is_string($content)) { - throw new Exception(sprintf('Tried to import content into the resource collection "%s" but the given content was a %s instead of a string.', $collectionName, gettype($content)), 1380878115); - } $this->initialize(); if (!isset($this->collections[$collectionName])) { throw new Exception(sprintf('Tried to import a file into the resource collection "%s" but no such collection exists. Please check your settings and the code which triggered the import.', $collectionName), 1380878131); } - /* @var CollectionInterface $collection */ $collection = $this->collections[$collectionName]; try { @@ -219,6 +216,7 @@ public function importResourceFromContent($content, $filename, $collectionName = throw new Exception(sprintf('Importing content into the resource collection "%s" failed: %s', $collectionName, $exception->getMessage()), 1381156155, $exception); } + /** @var PersistentResource $resource */ $this->resourceRepository->add($resource); $this->logger->debug(sprintf('Successfully imported content into the resource collection "%s" (storage: %s, a %s. SHA1: %s)', $collectionName, $collection->getStorage()->getName(), get_class($collection->getStorage()), $resource->getSha1())); @@ -232,7 +230,7 @@ public function importResourceFromContent($content, $filename, $collectionName = * On a successful import this method returns a PersistentResource object representing * the newly imported persistent resource. * - * @param array $uploadInfo An array detailing the resource to import (expected keys: name, tmp_name) + * @param array $uploadInfo An array detailing the resource to import (expected keys: name, tmp_name) * @param string $collectionName Name of the collection this uploaded resource should be added to * @return PersistentResource A resource object representing the imported resource * @throws Exception @@ -303,7 +301,7 @@ public function getStreamByResource(PersistentResource $resource) * $resource2 => array('originalFilename' => 'Bar.txt'), * ... * - * @return \SplObjectStorage + * @return \SplObjectStorage * @api */ public function getImportedResources() @@ -449,7 +447,7 @@ public function getPublicPackageResourceUriByPath($path) * Return the package key and the relative path and filename from the given resource path * * @param string $path The ressource path, like resource://Your.Package/Public/Image/Dummy.png - * @return array The array contains two value, first the packageKey followed by the relativePathAndFilename + * @return array{0: string, 1: string} The array contains two value, first the packageKey followed by the relativePathAndFilename * @throws Exception * @api */ @@ -554,7 +552,10 @@ protected function initializeStorages() if (!class_exists($storageDefinition['storage'])) { throw new Exception(sprintf('The configuration for the resource storage "%s" defined in your settings has not defined a valid "storage" option. Please check the configuration syntax and make sure that the specified class "%s" really exists.', $storageName, $storageDefinition['storage']), 1361467212); } - $options = (isset($storageDefinition['storageOptions']) ? $storageDefinition['storageOptions'] : []); + if (!is_subclass_of($storageDefinition['storage'], StorageInterface::class)) { + throw new Exception('Configured storage ' . $storageDefinition['storage'] . ' does not implement the required ' . StorageInterface::class, 1743970047); + } + $options = ($storageDefinition['storageOptions'] ?? []); $this->storages[$storageName] = new $storageDefinition['storage']($storageName, $options); } } @@ -574,7 +575,10 @@ protected function initializeTargets() if (!class_exists($targetDefinition['target'])) { throw new Exception(sprintf('The configuration for the resource target "%s" defined in your settings has not defined a valid "target" option. Please check the configuration syntax and make sure that the specified class "%s" really exists.', $targetName, $targetDefinition['target']), 1361467839); } - $options = (isset($targetDefinition['targetOptions']) ? $targetDefinition['targetOptions'] : []); + if (!is_subclass_of($targetDefinition['target'], TargetInterface::class)) { + throw new Exception('Configured resource target ' . $targetDefinition['target'] . ' does not implement the required ' . TargetInterface::class, 1743969958); + } + $options = $targetDefinition['targetOptions'] ?? []; $this->targets[$targetName] = new $targetDefinition['target']($targetName, $options); } } @@ -612,21 +616,24 @@ protected function initializeCollections() * Prepare an uploaded file to be imported as resource object. Will check the validity of the file, * move it outside of upload folder if open_basedir is enabled and check the filename. * - * @param array $uploadInfo - * @return array Array of string with the two keys "filepath" (the path to get the filecontent from) and "filename" the filename of the originally uploaded file. + * @param array $uploadInfo + * @return array{filepath: string, filename: string} Array of string with the two keys "filepath" (the path to get the filecontent from) and "filename" the filename of the originally uploaded file. * @throws Exception */ protected function prepareUploadedFileForImport(array $uploadInfo) { $openBasedirEnabled = (boolean)ini_get('open_basedir'); - $temporaryTargetPathAndFilename = $uploadInfo['tmp_name']; + $temporaryTargetPathAndFilename = $uploadInfo['tmp_name'] ?? null; + if (!is_string($temporaryTargetPathAndFilename)) { + throw new \Exception('String tmp_name required', 1743969722); + } $pathInfo = UnicodeFunctions::pathinfo($uploadInfo['name']); if (!is_uploaded_file($temporaryTargetPathAndFilename)) { throw new Exception('The given upload file "' . strip_tags($pathInfo['basename']) . '" was not uploaded through PHP. As it could pose a security risk it cannot be imported.', 1422461503); } - if (isset($pathInfo['extension']) && array_key_exists(strtolower($pathInfo['extension']), $this->settings['extensionsBlockedFromUpload']) && $this->settings['extensionsBlockedFromUpload'][strtolower($pathInfo['extension'])] === true) { + if (array_key_exists(strtolower($pathInfo['extension']), $this->settings['extensionsBlockedFromUpload']) && $this->settings['extensionsBlockedFromUpload'][strtolower($pathInfo['extension'])] === true) { throw new Exception('The extension of the given upload file "' . strip_tags($pathInfo['basename']) . '" is excluded. As it could pose a security risk it cannot be imported.', 1447148472); } @@ -645,7 +652,7 @@ protected function prepareUploadedFileForImport(array $uploadInfo) return [ 'filepath' => $temporaryTargetPathAndFilename, - 'filename' => $pathInfo['basename'] + 'filename' => $pathInfo['basename'], ]; } } diff --git a/Neos.Flow/Classes/ResourceManagement/ResourceMetaDataInterface.php b/Neos.Flow/Classes/ResourceManagement/ResourceMetaDataInterface.php index 8cd9b10a9d..89556eac2e 100644 --- a/Neos.Flow/Classes/ResourceManagement/ResourceMetaDataInterface.php +++ b/Neos.Flow/Classes/ResourceManagement/ResourceMetaDataInterface.php @@ -80,5 +80,5 @@ public function getSha1(); * @param string $sha1 The sha1 hash * @return void */ - public function setSha1($sha1); + public function setSha1(string $sha1); } diff --git a/Neos.Flow/Classes/ResourceManagement/ResourceRepository.php b/Neos.Flow/Classes/ResourceManagement/ResourceRepository.php index 82bb9dcf7a..605ba63894 100644 --- a/Neos.Flow/Classes/ResourceManagement/ResourceRepository.php +++ b/Neos.Flow/Classes/ResourceManagement/ResourceRepository.php @@ -48,12 +48,12 @@ class ResourceRepository extends Repository protected $persistenceManager; /** - * @var \SplObjectStorage|PersistentResource[] + * @var \SplObjectStorage */ protected $removedResources; /** - * @var \SplObjectStorage|PersistentResource[] + * @var \SplObjectStorage */ protected $addedResources; @@ -73,6 +73,9 @@ public function __construct() */ public function add($object): void { + if (!($object instanceof PersistentResource)) { + throw new \Exception('Can only add objects of type PersistentResource', 1743969457); + } $this->persistenceManager->allowObject($object); if ($this->removedResources->contains($object)) { $this->removedResources->detach($object); @@ -91,6 +94,9 @@ public function add($object): void */ public function remove($object): void { + if (!($object instanceof PersistentResource)) { + throw new \Exception('Can only remove objects of type PersistentResource', 1743969438); + } // Intercept a second call for the same PersistentResource object because it might cause an endless loop caused by // the ResourceManager's deleteResource() method which also calls this remove() function: if (!$this->removedResources->contains($object)) { @@ -188,7 +194,7 @@ public function findSimilarResources(PersistentResource $resource) * Find all resources with the same SHA1 hash * * @param string $sha1Hash - * @return array + * @return array */ public function findBySha1($sha1Hash) { @@ -209,7 +215,7 @@ public function findBySha1($sha1Hash) * * @param string $sha1Hash * @param string $collectionName - * @return array + * @return array */ public function findBySha1AndCollectionName($sha1Hash, $collectionName) { @@ -261,16 +267,15 @@ public function countBySha1AndCollectionName(string $sha1Hash, string $collectio * Find one resource by SHA1 * * @param string $sha1Hash - * @return PersistentResource + * @return ?PersistentResource */ public function findOneBySha1($sha1Hash) { $query = $this->createQuery(); $query->matching($query->equals('sha1', $sha1Hash))->setLimit(1); - /** @var PersistentResource $resource */ + /** @var ?PersistentResource $resource */ $resource = $query->execute()->getFirst(); if ($resource === null) { - /** @var PersistentResource $importedResource */ foreach ($this->addedResources as $importedResource) { if ($importedResource->getSha1() === $sha1Hash) { return $importedResource; @@ -282,7 +287,7 @@ public function findOneBySha1($sha1Hash) } /** - * @return \SplObjectStorage + * @return \SplObjectStorage */ public function getAddedResources() { diff --git a/Neos.Flow/Classes/ResourceManagement/ResourceTypeConverter.php b/Neos.Flow/Classes/ResourceManagement/ResourceTypeConverter.php index fc81836e5b..cb806ee1d2 100644 --- a/Neos.Flow/Classes/ResourceManagement/ResourceTypeConverter.php +++ b/Neos.Flow/Classes/ResourceManagement/ResourceTypeConverter.php @@ -119,7 +119,7 @@ class ResourceTypeConverter extends AbstractTypeConverter protected $logger; /** - * @var array + * @var array */ protected $convertedResources = []; @@ -144,9 +144,9 @@ public function injectLogger(LoggerInterface $logger) * Note that $source['error'] will also be present if a file was successfully * uploaded. In that case its value will be \UPLOAD_ERR_OK. * - * @param array|string|UploadedFileInterface $source The upload info (expected keys: error, name, tmp_name), the hash or an UploadedFile + * @param array|string|UploadedFileInterface $source The upload info (expected keys: error, name, tmp_name), the hash or an UploadedFile * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return PersistentResource|null|FlowError if the input format is not supported or could not be converted for other reasons * @throws Exception @@ -177,7 +177,7 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie } /** - * @param array $source + * @param array $source * @param PropertyMappingConfigurationInterface|null $configuration * @return PersistentResource|null|FlowError */ @@ -219,7 +219,7 @@ protected function handleFileUploads(array $source, ?PropertyMappingConfiguratio } /** - * @param array $source + * @param array $source * @param PropertyMappingConfigurationInterface|null $configuration * @return PersistentResource|FlowError * @throws Exception @@ -256,12 +256,13 @@ protected function handleHashAndData(array $source, ?PropertyMappingConfiguratio if (isset($source['data']) && isset($source['filename'])) { $resource = $this->resourceManager->importResourceFromContent(base64_decode($source['data']), $source['filename'], $collectionName, $givenResourceIdentity); } elseif ($hash !== null) { - $resource = $this->resourceManager->importResource($configuration->getConfigurationValue(ResourceTypeConverter::class, self::CONFIGURATION_RESOURCE_LOAD_PATH) . '/' . $hash, $collectionName, $givenResourceIdentity); - if (is_array($source) && isset($source['filename'])) { - $resource->setFilename($source['filename']); + $resource = $this->resourceManager->importResource($configuration?->getConfigurationValue(ResourceTypeConverter::class, self::CONFIGURATION_RESOURCE_LOAD_PATH) . '/' . $hash, $collectionName, $givenResourceIdentity); + $filename = $source['filename'] ?? null; + if (is_string($filename)) { + $resource->setFilename($filename); } } - if ($hash !== null && $resource->getSha1() !== $hash) { + if ($hash !== null && $resource?->getSha1() !== $hash) { throw new Exception\InvalidResourceDataException('The source SHA1 did not match the SHA1 of the imported resource.', 1482248149); } } @@ -297,7 +298,7 @@ protected function handleUploadedFile(UploadedFileInterface $source, ?PropertyMa case \UPLOAD_ERR_PARTIAL: return new FlowError(Files::getUploadErrorMessage($source->getError()), 1264440823); default: - $this->logger->error(sprintf('A server error occurred while converting an uploaded resource: "%s"', Files::getUploadErrorMessage($source['error'])), LogEnvironment::fromMethodName(__METHOD__)); + $this->logger->error(sprintf('A server error occurred while converting an uploaded resource: "%s"', Files::getUploadErrorMessage($source->getError())), LogEnvironment::fromMethodName(__METHOD__)); return new FlowError('An error occurred while uploading. Please try again or contact the administrator if the problem remains', 1340193849); } @@ -307,8 +308,15 @@ protected function handleUploadedFile(UploadedFileInterface $source, ?PropertyMa } try { - $resource = $this->resourceManager->importResource($source->getStream()->detach(), $this->getCollectionName($source, $configuration)); - $resource->setFilename($source->getClientFilename()); + $sourceForImport = $source->getStream()->detach(); + if ($sourceForImport === null) { + throw new \Exception('Failed to detach resource from stream', 1743967911); + } + $resource = $this->resourceManager->importResource($sourceForImport, $this->getCollectionName($source, $configuration)); + $filename = $source->getClientFilename(); + if ($filename !== null) { + $resource->setFilename($filename); + } $this->convertedResources[spl_object_hash($source)] = $resource; return $resource; } catch (\Exception $exception) { @@ -324,7 +332,7 @@ protected function handleUploadedFile(UploadedFileInterface $source, ?PropertyMa * The propertyMappingConfiguration CONFIGURATION_COLLECTION_NAME will directly override the default. Then if CONFIGURATION_ALLOW_COLLECTION_OVERRIDE is true * and __collectionName is in the $source this will finally be the value. * - * @param array|UploadedFileInterface $source + * @param array|UploadedFileInterface $source * @param PropertyMappingConfigurationInterface|null $configuration * @return string * @throws InvalidPropertyMappingConfigurationException diff --git a/Neos.Flow/Classes/ResourceManagement/Storage/FileSystemStorage.php b/Neos.Flow/Classes/ResourceManagement/Storage/FileSystemStorage.php index 6e4f0536cf..ad0ee6580c 100644 --- a/Neos.Flow/Classes/ResourceManagement/Storage/FileSystemStorage.php +++ b/Neos.Flow/Classes/ResourceManagement/Storage/FileSystemStorage.php @@ -59,7 +59,7 @@ class FileSystemStorage implements StorageInterface * Constructor * * @param string $name Name of this storage instance, according to the resource settings - * @param array $options Options for this storage + * @param array $options Options for this storage * @throws Exception */ public function __construct($name, array $options = []) diff --git a/Neos.Flow/Classes/ResourceManagement/Storage/PackageStorage.php b/Neos.Flow/Classes/ResourceManagement/Storage/PackageStorage.php index d0ff7285aa..23ead9adba 100644 --- a/Neos.Flow/Classes/ResourceManagement/Storage/PackageStorage.php +++ b/Neos.Flow/Classes/ResourceManagement/Storage/PackageStorage.php @@ -71,7 +71,10 @@ public function getObjectsByPathPattern($pattern) if ($directoryPattern === '*') { $directories[$packageKey][] = $package->getPackagePath(); } else { - $directories[$packageKey] = glob($package->getPackagePath() . $directoryPattern, GLOB_ONLYDIR); + $packageDirectories = glob($package->getPackagePath() . $directoryPattern, GLOB_ONLYDIR); + if ($packageDirectories !== false) { + $directories[$packageKey] = $packageDirectories; + } } } @@ -98,11 +101,15 @@ protected function createStorageObject($resourcePathAndFilename, FlowPackageInte $object = new StorageObject(); $object->setFilename($pathInfo['basename']); - $object->setSha1(sha1_file($resourcePathAndFilename)); - $object->setFileSize(filesize($resourcePathAndFilename)); - if (isset($pathInfo['dirname'])) { - $object->setRelativePublicationPath($this->prepareRelativePublicationPath($pathInfo['dirname'], $resourcePackage->getPackageKey(), $resourcePackage->getResourcesPath())); + $sha1 = sha1_file($resourcePathAndFilename); + if ($sha1 !== false) { + $object->setSha1($sha1); + } + $fileSize = filesize($resourcePathAndFilename); + if ($fileSize !== false) { + $object->setFileSize($fileSize); } + $object->setRelativePublicationPath($this->prepareRelativePublicationPath($pathInfo['dirname'], $resourcePackage->getPackageKey(), $resourcePackage->getResourcesPath())); $object->setStream(function () use ($resourcePathAndFilename) { return fopen($resourcePathAndFilename, 'r'); }); diff --git a/Neos.Flow/Classes/ResourceManagement/Storage/StorageObject.php b/Neos.Flow/Classes/ResourceManagement/Storage/StorageObject.php index 40dc365534..53d37a923e 100644 --- a/Neos.Flow/Classes/ResourceManagement/Storage/StorageObject.php +++ b/Neos.Flow/Classes/ResourceManagement/Storage/StorageObject.php @@ -98,7 +98,7 @@ public function getMediaType() public function setFilename($filename) { $pathInfo = UnicodeFunctions::pathinfo($filename); - $extension = (isset($pathInfo['extension']) ? '.' . strtolower($pathInfo['extension']) : ''); + $extension = '.' . strtolower($pathInfo['extension']); $this->filename = $pathInfo['filename'] . $extension; $this->mediaType = MediaTypes::getMediaTypeFromFilename($this->filename); } @@ -201,7 +201,11 @@ public function setStream($stream) public function getStream() { if ($this->stream instanceof \Closure) { - $this->stream = $this->stream->__invoke(); + $resource = $this->stream->__invoke(); + if (!is_resource($resource)) { + throw new \Exception('Failed to resolve resource from closure', 1743962813); + } + $this->stream = $resource; } if (is_resource($this->stream)) { $meta = stream_get_meta_data($this->stream); diff --git a/Neos.Flow/Classes/ResourceManagement/Storage/WritableFileSystemStorage.php b/Neos.Flow/Classes/ResourceManagement/Storage/WritableFileSystemStorage.php index 1974e75a6c..f841a53aa8 100644 --- a/Neos.Flow/Classes/ResourceManagement/Storage/WritableFileSystemStorage.php +++ b/Neos.Flow/Classes/ResourceManagement/Storage/WritableFileSystemStorage.php @@ -57,12 +57,16 @@ public function importResource($source, $collectionName) if (is_resource($source)) { try { $target = fopen($temporaryTargetPathAndFilename, 'wb'); + if ($target === false) { + throw new \Exception('Failed to open ' . $temporaryTargetPathAndFilename, 1743964877); + } stream_copy_to_stream($source, $target); fclose($target); } catch (\Exception $exception) { throw new StorageException(sprintf('Could import the content stream to temporary file "%s".', $temporaryTargetPathAndFilename), 1380880079); } } else { + /** @var string $source */ if (copy($source, $temporaryTargetPathAndFilename) === false) { throw new StorageException(sprintf('Could not copy the file from "%s" to temporary file "%s".', $source, $temporaryTargetPathAndFilename), 1375198876); } @@ -130,6 +134,9 @@ protected function importTemporaryFile($temporaryPathAndFileName, $collectionNam { $this->fixFilePermissions($temporaryPathAndFileName); $sha1Hash = sha1_file($temporaryPathAndFileName); + if ($sha1Hash === false) { + throw new \Exception('Failed to resolve sha1 hash for ' . $temporaryPathAndFileName); + } $targetPathAndFilename = $this->getStoragePathAndFilenameByHash($sha1Hash); if (!is_file($targetPathAndFilename)) { @@ -139,7 +146,10 @@ protected function importTemporaryFile($temporaryPathAndFileName, $collectionNam } $resource = new PersistentResource(); - $resource->setFileSize(filesize($targetPathAndFilename)); + $fileSize = filesize($targetPathAndFilename); + if ($fileSize !== false) { + $resource->setFileSize($fileSize); + } $resource->setCollectionName($collectionName); $resource->setSha1($sha1Hash); diff --git a/Neos.Flow/Classes/ResourceManagement/Streams/ResourceStreamWrapper.php b/Neos.Flow/Classes/ResourceManagement/Streams/ResourceStreamWrapper.php index 752ce47558..6117b75326 100644 --- a/Neos.Flow/Classes/ResourceManagement/Streams/ResourceStreamWrapper.php +++ b/Neos.Flow/Classes/ResourceManagement/Streams/ResourceStreamWrapper.php @@ -88,7 +88,7 @@ public function closeDirectory() * This method is called in response to opendir(). * * @param string $path Specifies the URL that was passed to opendir(). - * @param int $options Whether or not to enforce safe_mode (0x04). + * @param int $options Whether to enforce safe_mode (0x04). * @return boolean true on success or false on failure. */ public function openDirectory($path, $options) @@ -97,7 +97,7 @@ public function openDirectory($path, $options) if (!is_string($resourceUriOrStream)) { return false; } - $handle = ($resourceUriOrStream !== false) ? opendir($resourceUriOrStream) : false; + $handle = opendir($resourceUriOrStream); if ($handle !== false) { $this->handle = $handle; return true; @@ -110,7 +110,7 @@ public function openDirectory($path, $options) * * This method is called in response to readdir(). * - * @return string Should return string representing the next filename, or false if there is no next file. + * @return string|false Should return string representing the next filename, or false if there is no next file. */ public function readDirectory() { @@ -148,7 +148,7 @@ public function makeDirectory($path, $mode, $options) { $resourceUriOrStream = $this->evaluateResourcePath($path, false); if (is_string($resourceUriOrStream)) { - return mkdir($resourceUriOrStream, $mode, $options&STREAM_MKDIR_RECURSIVE); + return mkdir($resourceUriOrStream, $mode, (bool)($options&STREAM_MKDIR_RECURSIVE)); } return false; } @@ -309,10 +309,11 @@ public function open($path, $mode, $options, &$openedPathAndFilename) $this->handle = $resourceUriOrStream; return true; } - + /** @var string|false $resourceUriOrStream */ $handle = ($resourceUriOrStream !== false) ? fopen($resourceUriOrStream, $mode) : false; if ($handle !== false) { $this->handle = $handle; + /** @var string $resourceUriOrStream */ $openedPathAndFilename = $resourceUriOrStream; return true; } @@ -328,10 +329,13 @@ public function open($path, $mode, $options, &$openedPathAndFilename) * number of bytes that were successfully read). * * @param integer $count How many bytes of data from the current position should be returned. - * @return string If there are less than count bytes available, return as many as are available. If no more data is available, return either false or an empty string. + * @return string|false If there are less than count bytes available, return as many as are available. If no more data is available, return either false or an empty string. */ public function read($count) { + if ($count < 1) { + throw new \Exception('Cannot read less than one byte', 1743964166); + } return fread($this->handle, $count); } @@ -392,7 +396,7 @@ public function setOption($option, $argument1, $argument2) * * This method is called in response to ftell(). * - * @return int Should return the current position of the stream. + * @return int|false Should return the current position of the stream. */ public function tell() { @@ -411,7 +415,7 @@ public function tell() * bytes that were successfully written. * * @param string $data Should be stored into the underlying stream. - * @return int Should return the number of bytes that were successfully stored, or 0 if none could be stored. + * @return int|false Should return the number of bytes that were successfully stored, or 0 if none could be stored. */ public function write($data) { @@ -441,7 +445,7 @@ public function unlink($path) * * This method is called in response to fstat(). * - * @return array See http://php.net/stat + * @return array|false See http://php.net/stat */ public function resourceStat() { @@ -472,14 +476,18 @@ public function resourceStat() * * @param string $path The file path or URL to stat. Note that in the case of a URL, it must be a :// delimited URL. Other URL forms are not supported. * @param integer $flags Holds additional flags set by the streams API. - * @return array Should return as many elements as stat() does. Unknown or unavailable values should be set to a rational value (usually 0). + * @return array|false Should return as many elements as stat() does. Unknown or unavailable values should be set to a rational value (usually 0). */ public function pathStat($path, $flags) { $evaluatedResourcePath = $this->evaluateResourcePath($path); + if ($evaluatedResourcePath === false) { + return false; + } if (is_resource($evaluatedResourcePath)) { return @fstat($evaluatedResourcePath); } + /** @var string $evaluatedResourcePath */ return @stat($evaluatedResourcePath); } @@ -489,7 +497,7 @@ public function pathStat($path, $flags) * * @param string $requestedPath * @param boolean $checkForExistence Whether a (non-hash) path should be checked for existence before being returned - * @return mixed The full path and filename or false if the file doesn't exist + * @return string|resource|false The full path and filename or false if the file doesn't exist * @throws \InvalidArgumentException|ResourceException */ protected function evaluateResourcePath($requestedPath, $checkForExistence = true) @@ -507,7 +515,7 @@ protected function evaluateResourcePath($requestedPath, $checkForExistence = tru if (strpos($resourceUriWithoutScheme, '/') === false && preg_match('/^[0-9a-f]{40}$/i', $resourceUriWithoutScheme) === 1) { $resource = $this->resourceManager->getResourceBySha1($resourceUriWithoutScheme); - return $this->resourceManager->getStreamByResource($resource); + return $resource ? $this->resourceManager->getStreamByResource($resource) : false; } list($packageName, $path) = explode('/', $resourceUriWithoutScheme, 2); diff --git a/Neos.Flow/Classes/ResourceManagement/Streams/StreamWrapperAdapter.php b/Neos.Flow/Classes/ResourceManagement/Streams/StreamWrapperAdapter.php index adec60d7fc..a75a876278 100644 --- a/Neos.Flow/Classes/ResourceManagement/Streams/StreamWrapperAdapter.php +++ b/Neos.Flow/Classes/ResourceManagement/Streams/StreamWrapperAdapter.php @@ -28,7 +28,7 @@ class StreamWrapperAdapter { /** - * @var array + * @var array> indexed by scheme */ protected static $registeredStreamWrappers = []; @@ -51,7 +51,6 @@ class StreamWrapperAdapter public static function initializeStreamWrapper($objectManager) { $streamWrapperClassNames = static::getStreamWrapperImplementationClassNames($objectManager); - /** @var StreamWrapperInterface $streamWrapperClassName */ foreach ($streamWrapperClassNames as $streamWrapperClassName) { $scheme = $streamWrapperClassName::getScheme(); if (in_array($scheme, stream_get_wrappers())) { @@ -67,7 +66,7 @@ public static function initializeStreamWrapper($objectManager) * earlier ones without warning. * * @param string $scheme - * @param string $objectName + * @param class-string $objectName * @return void */ public static function registerStreamWrapper($scheme, $objectName) @@ -78,7 +77,7 @@ public static function registerStreamWrapper($scheme, $objectName) /** * Returns the stream wrappers registered with this class. * - * @return array + * @return array> indexed by scheme */ public static function getRegisteredStreamWrappers() { @@ -226,7 +225,7 @@ public function rmdir($path, $options) * This method is called in response to stream_select(). * * @param integer $cast_as Can be STREAM_CAST_FOR_SELECT when stream_select() is calling stream_cast() or STREAM_CAST_AS_STREAM when stream_cast() is called for other uses. - * @return resource Should return the underlying stream resource used by the wrapper, or false. + * @return resource|false Should return the underlying stream resource used by the wrapper, or false. */ public function stream_cast($cast_as) { @@ -401,7 +400,7 @@ public function stream_set_option($option, $arg1, $arg2) * * This method is called in response to fstat(). * - * @return array See http://php.net/stat + * @return array See http://php.net/stat */ public function stream_stat() { @@ -476,7 +475,7 @@ public function unlink($path) * * @param string $path The file path or URL to stat. Note that in the case of a URL, it must be a :// delimited URL. Other URL forms are not supported. * @param integer $flags Holds additional flags set by the streams API. - * @return array Should return as many elements as stat() does. Unknown or unavailable values should be set to a rational value (usually 0). + * @return array|false Should return as many elements as stat() does. Unknown or unavailable values should be set to a rational value (usually 0). */ public function url_stat($path, $flags) { @@ -488,7 +487,7 @@ public function url_stat($path, $flags) * Returns all class names implementing the StreamWrapperInterface. * * @param ObjectManagerInterface $objectManager - * @return array Array of stream wrapper implementations + * @return array> Array of stream wrapper implementations * @Flow\CompileStatic */ public static function getStreamWrapperImplementationClassNames(ObjectManagerInterface $objectManager) diff --git a/Neos.Flow/Classes/ResourceManagement/Streams/StreamWrapperInterface.php b/Neos.Flow/Classes/ResourceManagement/Streams/StreamWrapperInterface.php index 13365e92c6..11e91d18a1 100644 --- a/Neos.Flow/Classes/ResourceManagement/Streams/StreamWrapperInterface.php +++ b/Neos.Flow/Classes/ResourceManagement/Streams/StreamWrapperInterface.php @@ -335,7 +335,7 @@ public function unlink($path); * * This method is called in response to fstat(). * - * @return array See http://php.net/stat + * @return array See http://php.net/stat * @api */ public function resourceStat(); @@ -359,7 +359,7 @@ public function resourceStat(); * * @param string $path The file path or URL to stat. Note that in the case of a URL, it must be a :// delimited URL. Other URL forms are not supported. * @param integer $flags Holds additional flags set by the streams API. - * @return array Should return as many elements as stat() does. Unknown or unavailable values should be set to a rational value (usually 0). + * @return array|false Should return as many elements as stat() does. Unknown or unavailable values should be set to a rational value (usually 0). * @api */ public function pathStat($path, $flags); diff --git a/Neos.Flow/Classes/ResourceManagement/Target/FileSystemSymlinkTarget.php b/Neos.Flow/Classes/ResourceManagement/Target/FileSystemSymlinkTarget.php index f6e808cdbe..34a83aac88 100644 --- a/Neos.Flow/Classes/ResourceManagement/Target/FileSystemSymlinkTarget.php +++ b/Neos.Flow/Classes/ResourceManagement/Target/FileSystemSymlinkTarget.php @@ -62,7 +62,7 @@ protected function publishFile($sourceStream, $relativeTargetPathAndFilename) { $extension = strtolower(pathinfo($relativeTargetPathAndFilename, PATHINFO_EXTENSION)); if ($extension !== '' && array_key_exists($extension, $this->excludedExtensions) && $this->excludedExtensions[$extension] === true) { - throw new TargetException(sprintf('Could not publish "%s" into resource publishing target "%s" because the filename extension "%s" is excluded.', $sourceStream, $this->name, $extension), 1447152230); + throw new TargetException(sprintf('Could not publish resource into publishing target "%s" because the filename extension "%s" is excluded.', $this->name, $extension), 1447152230); } $streamMetaData = stream_get_meta_data($sourceStream); @@ -151,6 +151,9 @@ protected function setOption($key, $value) return parent::setOption($key, $value); } + /** + * @return array{0: bool, 1: ?\Exception} + */ private function publish(string $targetPathAndFilename, string $sourcePathAndFilename): array { $exception = null; diff --git a/Neos.Flow/Classes/ResourceManagement/Target/FileSystemTarget.php b/Neos.Flow/Classes/ResourceManagement/Target/FileSystemTarget.php index f30d54274b..3d6c8bf149 100644 --- a/Neos.Flow/Classes/ResourceManagement/Target/FileSystemTarget.php +++ b/Neos.Flow/Classes/ResourceManagement/Target/FileSystemTarget.php @@ -33,7 +33,7 @@ class FileSystemTarget implements TargetInterface { /** - * @var array + * @var array */ protected $options = []; @@ -83,7 +83,7 @@ class FileSystemTarget implements TargetInterface /** * A list of extensions that are excluded and must not be published by this target. * - * @var array + * @var array */ protected $excludedExtensions = []; @@ -115,7 +115,7 @@ class FileSystemTarget implements TargetInterface * Constructor * * @param string $name Name of this target instance, according to the resource settings - * @param array $options Options for this target + * @param array $options Options for this target */ public function __construct($name, array $options = []) { @@ -217,8 +217,9 @@ public function publishCollection(CollectionInterface $collection) $this->checkAndRemovePackageSymlinks($storage); $iteration = 0; foreach ($collection->getObjects() as $object) { - $sourceStream = $object->getStream(); - if ($sourceStream === false) { + try { + $sourceStream = $object->getStream(); + } catch (\Exception) { $this->handleMissingData($object, $collection); continue; } @@ -255,7 +256,7 @@ public function publishResource(PersistentResource $resource, CollectionInterfac * @param ResourceMetaDataInterface $resource * @param CollectionInterface $collection */ - protected function handleMissingData(ResourceMetaDataInterface $resource, CollectionInterface $collection) + protected function handleMissingData(ResourceMetaDataInterface $resource, CollectionInterface $collection): void { $message = sprintf('Could not publish resource %s with SHA1 hash %s of collection %s because there seems to be no corresponding data in the storage.', $resource->getFilename(), $resource->getSha1(), $collection->getName()); $this->messageCollector->append($message); @@ -323,16 +324,16 @@ protected function encodeRelativePathAndFilenameForUri($relativePathAndFilename) protected function publishFile($sourceStream, $relativeTargetPathAndFilename) { $pathInfo = UnicodeFunctions::pathinfo($relativeTargetPathAndFilename); - if (isset($pathInfo['extension']) && array_key_exists(strtolower($pathInfo['extension']), $this->excludedExtensions) && $this->excludedExtensions[strtolower($pathInfo['extension'])] === true) { - throw new TargetException(sprintf('Could not publish "%s" into resource publishing target "%s" because the filename extension "%s" is excluded.', $sourceStream, $this->name, strtolower($pathInfo['extension'])), 1447148472); + if (array_key_exists(strtolower($pathInfo['extension']), $this->excludedExtensions) && $this->excludedExtensions[strtolower($pathInfo['extension'])] === true) { + throw new TargetException(sprintf('Could not publish resource into publishing target "%s" because the filename extension "%s" is excluded.', $this->name, strtolower($pathInfo['extension'])), 1447148472); } $targetPathAndFilename = $this->path . $relativeTargetPathAndFilename; $streamMetaData = stream_get_meta_data($sourceStream); - $sourcePathAndFilename = $streamMetaData['uri'] ?? null; + $sourcePathAndFilename = $streamMetaData['uri']; if (@fstat($sourceStream) === false) { - throw new TargetException(sprintf('Could not publish "%s" into resource publishing target "%s" because the source file is not accessible (file stat failed).', $sourceStream, $this->name), 1375258499); + throw new TargetException(sprintf('Could not publish resource into publishing target "%s" because the source file is not accessible (file stat failed).', $this->name), 1375258499); } // If you switch from FileSystemSymlinkTarget than we need to remove the symlink before trying to write the file @@ -346,7 +347,7 @@ protected function publishFile($sourceStream, $relativeTargetPathAndFilename) } if (!is_writable(dirname($targetPathAndFilename))) { - throw new Exception(sprintf('Could not publish "%s" into resource publishing target "%s" because the target file "%s" is not writable.', $sourceStream, $this->name, $targetPathAndFilename), 1428917322); + throw new Exception(sprintf('Could not publish resource into publishing target "%s" because the target file "%s" is not writable.', $this->name, $targetPathAndFilename), 1428917322); } try { @@ -361,7 +362,7 @@ protected function publishFile($sourceStream, $relativeTargetPathAndFilename) $result = false; } if ($result === false) { - throw new TargetException(sprintf('Could not publish "%s" into resource publishing target "%s" because the source file could not be copied to the target location "%s".', $sourceStream, $this->name, $targetPathAndFilename), 1375258399, ($exception ?? null)); + throw new TargetException(sprintf('Could not publish resource into publishing target "%s" because the source file could not be copied to the target location "%s".', $this->name, $targetPathAndFilename), 1375258399, ($exception ?? null)); } $this->logger->debug(sprintf('FileSystemTarget: Published file. (target: %s, file: %s)', $this->name, $relativeTargetPathAndFilename)); diff --git a/Neos.Flow/Classes/Security/Account.php b/Neos.Flow/Classes/Security/Account.php index 59afe04d2f..edd33c6b55 100644 --- a/Neos.Flow/Classes/Security/Account.php +++ b/Neos.Flow/Classes/Security/Account.php @@ -55,7 +55,7 @@ class Account protected $creationDate; /** - * @var \DateTime + * @var \DateTime|null * @ORM\Column(nullable=true) */ protected $expirationDate; @@ -73,7 +73,7 @@ class Account protected $failedAuthenticationCount; /** - * @var array of strings + * @var array * @ORM\Column(type="simple_array", nullable=true) */ protected $roleIdentifiers = []; @@ -225,9 +225,6 @@ public function setRoles(array $roles) $this->roleIdentifiers = []; $this->roles = []; foreach ($roles as $role) { - if (!$role instanceof Role) { - throw new \InvalidArgumentException(sprintf('setRoles() only accepts an array of %s instances, given: "%s"', Role::class, gettype($role)), 1397125997); - } $this->addRole($role); } } @@ -310,7 +307,7 @@ public function getExpirationDate() /** * Sets the date on which this account will become inactive * - * @param \DateTime $expirationDate + * @param ?\DateTime $expirationDate * @return void * @api */ diff --git a/Neos.Flow/Classes/Security/AccountFactory.php b/Neos.Flow/Classes/Security/AccountFactory.php index e90acf5f0f..69a62229ff 100644 --- a/Neos.Flow/Classes/Security/AccountFactory.php +++ b/Neos.Flow/Classes/Security/AccountFactory.php @@ -37,7 +37,7 @@ class AccountFactory * * @param string $identifier Identifier of the account, must be unique * @param string $password The clear text password - * @param array $roleIdentifiers Optionally an array of role identifiers to assign to the new account + * @param array $roleIdentifiers Optionally an array of role identifiers to assign to the new account * @param string $authenticationProviderName Optional name of the authentication provider the account is affiliated with * @param string $passwordHashingStrategy Optional password hashing strategy to use for the password * @return Account A new account, not yet added to the account repository diff --git a/Neos.Flow/Classes/Security/AccountRepository.php b/Neos.Flow/Classes/Security/AccountRepository.php index b305695968..293274335e 100644 --- a/Neos.Flow/Classes/Security/AccountRepository.php +++ b/Neos.Flow/Classes/Security/AccountRepository.php @@ -30,7 +30,7 @@ class AccountRepository extends Repository const ENTITY_CLASSNAME = Account::class; /** - * @var array + * @var array */ protected $defaultOrderings = ['creationDate' => QueryInterface::ORDER_DESCENDING]; @@ -51,6 +51,9 @@ class AccountRepository extends Repository */ public function remove($object): void { + if (!($object instanceof Account)) { + throw new \InvalidArgumentException('Can only remove account objects.', 1743961393); + } parent::remove($object); // destroy the sessions for the account to be removed @@ -67,12 +70,14 @@ public function remove($object): void public function findByAccountIdentifierAndAuthenticationProviderName($accountIdentifier, $authenticationProviderName) { $query = $this->createQuery(); - return $query->matching( + $result = $query->matching( $query->logicalAnd( $query->equals('accountIdentifier', $accountIdentifier), $query->equals('authenticationProviderName', $authenticationProviderName) ) )->execute()->getFirst(); + + return $result instanceof Account ? $result : null; } /** @@ -85,7 +90,7 @@ public function findByAccountIdentifierAndAuthenticationProviderName($accountIde public function findActiveByAccountIdentifierAndAuthenticationProviderName($accountIdentifier, $authenticationProviderName) { $query = $this->createQuery(); - return $query->matching( + $result = $query->matching( $query->logicalAnd( $query->equals('accountIdentifier', $accountIdentifier), $query->equals('authenticationProviderName', $authenticationProviderName), @@ -95,5 +100,7 @@ public function findActiveByAccountIdentifierAndAuthenticationProviderName($acco ) ) )->execute()->getFirst(); + + return $result instanceof Account ? $result : null; } } diff --git a/Neos.Flow/Classes/Security/Aspect/LoggingAspect.php b/Neos.Flow/Classes/Security/Aspect/LoggingAspect.php index e85afc9ac1..ae4271ee8c 100644 --- a/Neos.Flow/Classes/Security/Aspect/LoggingAspect.php +++ b/Neos.Flow/Classes/Security/Aspect/LoggingAspect.php @@ -100,9 +100,9 @@ public function logManagerLogout(JoinPointInterface $joinPoint) } /** - * @param array $collectedIdentifiers + * @param array $collectedIdentifiers * @param TokenInterface $token - * @return array + * @return array */ protected function reduceTokenToAccountIdentifier(array $collectedIdentifiers, TokenInterface $token): array { @@ -170,7 +170,7 @@ public function logPrivilegeAccessDecisions(JoinPointInterface $joinPoint) /** * @param JoinPointInterface $joinPoint - * @return array + * @return array */ protected function getLogEnvironmentFromJoinPoint(JoinPointInterface $joinPoint): array { diff --git a/Neos.Flow/Classes/Security/Authentication/AuthenticationProviderInterface.php b/Neos.Flow/Classes/Security/Authentication/AuthenticationProviderInterface.php index 965c39a1d1..8f3dbf3b6a 100644 --- a/Neos.Flow/Classes/Security/Authentication/AuthenticationProviderInterface.php +++ b/Neos.Flow/Classes/Security/Authentication/AuthenticationProviderInterface.php @@ -21,7 +21,7 @@ interface AuthenticationProviderInterface * Constructs an instance with the given name and options. * * @param string $name - * @param array $options + * @param array $options * @return self */ public static function create(string $name, array $options); @@ -37,7 +37,7 @@ public function canAuthenticate(TokenInterface $token); /** * Returns the classnames of the tokens this provider is responsible for. * - * @return array The classname of the token this provider is responsible for + * @return array> The classname of the token this provider is responsible for */ public function getTokenClassNames(); diff --git a/Neos.Flow/Classes/Security/Authentication/AuthenticationProviderManager.php b/Neos.Flow/Classes/Security/Authentication/AuthenticationProviderManager.php index ed40991eab..5746d2c08f 100644 --- a/Neos.Flow/Classes/Security/Authentication/AuthenticationProviderManager.php +++ b/Neos.Flow/Classes/Security/Authentication/AuthenticationProviderManager.php @@ -51,7 +51,7 @@ class AuthenticationProviderManager implements AuthenticationManagerInterface * Injected configuration for providers. * Will be null'd again after building the object instances. * - * @var array|null + * @var array|null */ protected $providerConfigurations; @@ -81,7 +81,7 @@ public function __construct(TokenAndProviderFactoryInterface $tokenAndProviderFa /** * Inject the settings and does a fresh build of tokens based on the injected settings * - * @param array $settings The settings + * @param array $settings The settings * @return void * @throws Exception */ @@ -206,7 +206,10 @@ public function isAuthenticated(): bool } catch (AuthenticationRequiredException $exception) { } } - return $this->isAuthenticated; + + return $this->isAuthenticated !== null + ? $this->isAuthenticated + : false; } /** diff --git a/Neos.Flow/Classes/Security/Authentication/AuthenticationProviderResolver.php b/Neos.Flow/Classes/Security/Authentication/AuthenticationProviderResolver.php index d04def1887..30bf153f7c 100644 --- a/Neos.Flow/Classes/Security/Authentication/AuthenticationProviderResolver.php +++ b/Neos.Flow/Classes/Security/Authentication/AuthenticationProviderResolver.php @@ -47,12 +47,12 @@ public function __construct(ObjectManagerInterface $objectManager) public function resolveProviderClass($providerName) { $className = $this->objectManager->getClassNameByObjectName($providerName); - if ($className !== false) { + if ($className !== false && is_subclass_of($className, AuthenticationProviderInterface::class)) { return $className; } $className = $this->objectManager->getClassNameByObjectName('Neos\Flow\Security\Authentication\Provider\\' . $providerName); - if ($className !== false) { + if ($className !== false && is_subclass_of($className, AuthenticationProviderInterface::class)) { return $className; } diff --git a/Neos.Flow/Classes/Security/Authentication/Controller/AbstractAuthenticationController.php b/Neos.Flow/Classes/Security/Authentication/Controller/AbstractAuthenticationController.php index 0c5fe443a5..7ac7f4aae6 100644 --- a/Neos.Flow/Classes/Security/Authentication/Controller/AbstractAuthenticationController.php +++ b/Neos.Flow/Classes/Security/Authentication/Controller/AbstractAuthenticationController.php @@ -85,7 +85,11 @@ public function authenticateAction() if (!$this->authenticationManager->isAuthenticated()) { $this->onAuthenticationFailure($authenticationException); - return call_user_func([$this, $this->errorMethodName]); + $callable = [$this, $this->errorMethodName]; + if (!is_callable($callable)) { + throw new \Exception('Invalid error method ' . $this->errorMethodName, 1743955562); + } + return call_user_func($callable); } $storedRequest = $this->securityContext->getInterceptedRequest(); diff --git a/Neos.Flow/Classes/Security/Authentication/EntryPoint/AbstractEntryPoint.php b/Neos.Flow/Classes/Security/Authentication/EntryPoint/AbstractEntryPoint.php index f11c1d5e60..c609acbd56 100644 --- a/Neos.Flow/Classes/Security/Authentication/EntryPoint/AbstractEntryPoint.php +++ b/Neos.Flow/Classes/Security/Authentication/EntryPoint/AbstractEntryPoint.php @@ -21,14 +21,14 @@ abstract class AbstractEntryPoint implements EntryPointInterface /** * The configurations options * - * @var array + * @var array */ protected $options = []; /** * Sets the options array * - * @param array $options An array of configuration options + * @param array $options An array of configuration options * @return void */ public function setOptions(array $options) @@ -39,7 +39,7 @@ public function setOptions(array $options) /** * Returns the options array * - * @return array The configuration options of this entry point + * @return array The configuration options of this entry point */ public function getOptions() { diff --git a/Neos.Flow/Classes/Security/Authentication/EntryPoint/WebRedirect.php b/Neos.Flow/Classes/Security/Authentication/EntryPoint/WebRedirect.php index a37757f4b0..b0aacac10c 100644 --- a/Neos.Flow/Classes/Security/Authentication/EntryPoint/WebRedirect.php +++ b/Neos.Flow/Classes/Security/Authentication/EntryPoint/WebRedirect.php @@ -75,7 +75,7 @@ public function startAuthentication(ServerRequestInterface $request, ResponseInt } /** - * @param array $routeValues + * @param array $routeValues * @param ServerRequestInterface $request * @return string * @throws \Neos\Flow\Mvc\Routing\Exception\MissingActionNameException @@ -96,7 +96,7 @@ protected function generateUriFromRouteValues(array $routeValues, ServerRequestI * Returns the entry $key from the array $routeValues removing the original array item. * If $key does not exist, NULL is returned. * - * @param array $routeValues + * @param array $routeValues * @param string $key * @return mixed the specified route value or NULL if it is not set */ diff --git a/Neos.Flow/Classes/Security/Authentication/EntryPointInterface.php b/Neos.Flow/Classes/Security/Authentication/EntryPointInterface.php index 9fba7eb5ac..3db795974b 100644 --- a/Neos.Flow/Classes/Security/Authentication/EntryPointInterface.php +++ b/Neos.Flow/Classes/Security/Authentication/EntryPointInterface.php @@ -22,7 +22,7 @@ interface EntryPointInterface /** * Sets the options array * - * @param array $options An array of configuration options + * @param array $options An array of configuration options * @return void */ public function setOptions(array $options); @@ -30,7 +30,7 @@ public function setOptions(array $options); /** * Returns the options array * - * @return array An array of configuration options + * @return array An array of configuration options */ public function getOptions(); diff --git a/Neos.Flow/Classes/Security/Authentication/Provider/AbstractProvider.php b/Neos.Flow/Classes/Security/Authentication/Provider/AbstractProvider.php index 7ddcac499c..c518e7bd8c 100644 --- a/Neos.Flow/Classes/Security/Authentication/Provider/AbstractProvider.php +++ b/Neos.Flow/Classes/Security/Authentication/Provider/AbstractProvider.php @@ -26,7 +26,7 @@ abstract class AbstractProvider implements AuthenticationProviderInterface protected $name; /** - * @var array + * @var array */ protected $options = []; @@ -34,7 +34,7 @@ abstract class AbstractProvider implements AuthenticationProviderInterface * Factory method * * @param string $name - * @param array $options + * @param array $options * @return AuthenticationProviderInterface * @api */ @@ -47,7 +47,7 @@ public static function create(string $name, array $options) * Protected constructor, see create method * * @param string $name The name of this authentication provider - * @param array $options Additional configuration options + * @param array $options Additional configuration options * @see create */ protected function __construct($name, array $options = []) diff --git a/Neos.Flow/Classes/Security/Authentication/Provider/FileBasedSimpleKeyProvider.php b/Neos.Flow/Classes/Security/Authentication/Provider/FileBasedSimpleKeyProvider.php index 5f92aa376a..f256d13b8a 100644 --- a/Neos.Flow/Classes/Security/Authentication/Provider/FileBasedSimpleKeyProvider.php +++ b/Neos.Flow/Classes/Security/Authentication/Provider/FileBasedSimpleKeyProvider.php @@ -66,7 +66,7 @@ class FileBasedSimpleKeyProvider extends AbstractProvider /** * Returns the class names of the tokens this provider can authenticate. * - * @return array + * @return array> */ public function getTokenClassNames() { @@ -106,7 +106,8 @@ public function authenticate(TokenInterface $authenticationToken) */ protected function validateCredentials(PasswordTokenInterface $authenticationToken): void { - if (!$this->hashService->validatePassword($authenticationToken->getPassword(), $this->fileBasedSimpleKeyService->getKey($this->options['keyName']))) { + $key = $this->fileBasedSimpleKeyService->getKey($this->options['keyName']); + if (!$this->hashService->validatePassword($authenticationToken->getPassword(), $key)) { $authenticationToken->setAuthenticationStatus(TokenInterface::WRONG_CREDENTIALS); return; } diff --git a/Neos.Flow/Classes/Security/Authentication/Provider/PersistedUsernamePasswordProvider.php b/Neos.Flow/Classes/Security/Authentication/Provider/PersistedUsernamePasswordProvider.php index 4c14105b59..7861c532bc 100644 --- a/Neos.Flow/Classes/Security/Authentication/Provider/PersistedUsernamePasswordProvider.php +++ b/Neos.Flow/Classes/Security/Authentication/Provider/PersistedUsernamePasswordProvider.php @@ -62,7 +62,7 @@ class PersistedUsernamePasswordProvider extends AbstractProvider /** * Returns the class names of the tokens this provider can authenticate. * - * @return array + * @return array> */ public function getTokenClassNames() { diff --git a/Neos.Flow/Classes/Security/Authentication/Provider/TestingProvider.php b/Neos.Flow/Classes/Security/Authentication/Provider/TestingProvider.php index cff0310b51..7fc46ea159 100644 --- a/Neos.Flow/Classes/Security/Authentication/Provider/TestingProvider.php +++ b/Neos.Flow/Classes/Security/Authentication/Provider/TestingProvider.php @@ -34,7 +34,7 @@ class TestingProvider extends AbstractProvider /** * Returns the class names of the tokens this provider can authenticate. * - * @return array + * @return array> */ public function getTokenClassNames() { diff --git a/Neos.Flow/Classes/Security/Authentication/Token/AbstractToken.php b/Neos.Flow/Classes/Security/Authentication/Token/AbstractToken.php index 42900ce9f6..6e232467dd 100644 --- a/Neos.Flow/Classes/Security/Authentication/Token/AbstractToken.php +++ b/Neos.Flow/Classes/Security/Authentication/Token/AbstractToken.php @@ -36,18 +36,18 @@ abstract class AbstractToken implements TokenInterface /** * The credentials submitted by the client - * @var array + * @var array * @Flow\Transient */ protected $credentials = []; /** - * @var Account + * @var ?Account */ protected $account; /** - * @var array + * @var array */ protected $requestPatterns = []; @@ -59,14 +59,14 @@ abstract class AbstractToken implements TokenInterface /** * Token options - * @var array + * @var array */ protected $options; /** * Build an instance of this token, potentially passing it options that can be configured via `tokenOptions` * - * @param array|null $options + * @param array|null $options */ public function __construct(?array $options = null) { @@ -118,7 +118,7 @@ public function setAuthenticationEntryPoint(EntryPointInterface $entryPoint) /** * Returns the configured authentication entry point, NULL if none is available * - * @return EntryPointInterface The configured authentication entry point, NULL if none is available + * @return ?EntryPointInterface The configured authentication entry point, NULL if none is available */ public function getAuthenticationEntryPoint() { @@ -138,7 +138,7 @@ public function hasRequestPatterns() /** * Sets request patterns * - * @param array $requestPatterns Array of RequestPatternInterface to be set + * @param array $requestPatterns Array of RequestPatternInterface to be set * @return void * @throws \InvalidArgumentException */ @@ -166,7 +166,7 @@ public function getRequestPatterns() /** * Returns the credentials (username and password) of this token. * - * @return array $credentials The needed credentials to authenticate this token + * @return array $credentials The needed credentials to authenticate this token */ public function getCredentials() { @@ -176,7 +176,7 @@ public function getCredentials() /** * Returns the account if one is authenticated, NULL otherwise. * - * @return Account An account object + * @return ?Account An account object */ public function getAccount() { @@ -186,7 +186,7 @@ public function getAccount() /** * Set the (authenticated) account * - * @param Account $account An account object + * @param ?Account $account An account object * @return void */ public function setAccount(?Account $account = null) diff --git a/Neos.Flow/Classes/Security/Authentication/Token/BearerToken.php b/Neos.Flow/Classes/Security/Authentication/Token/BearerToken.php index e51e3960ef..f09688a55a 100644 --- a/Neos.Flow/Classes/Security/Authentication/Token/BearerToken.php +++ b/Neos.Flow/Classes/Security/Authentication/Token/BearerToken.php @@ -30,7 +30,7 @@ class BearerToken extends AbstractToken implements SessionlessTokenInterface { /** * The password credentials - * @var array + * @var array{bearer: string} * @Flow\Transient */ protected $credentials = ['bearer' => '']; diff --git a/Neos.Flow/Classes/Security/Authentication/Token/PasswordToken.php b/Neos.Flow/Classes/Security/Authentication/Token/PasswordToken.php index 90732a5ec1..2ecfaf9bbb 100644 --- a/Neos.Flow/Classes/Security/Authentication/Token/PasswordToken.php +++ b/Neos.Flow/Classes/Security/Authentication/Token/PasswordToken.php @@ -25,7 +25,7 @@ class PasswordToken extends AbstractToken implements PasswordTokenInterface /** * The password credentials - * @var array + * @var array{password: string,} * @Flow\Transient */ protected $credentials = ['password' => '']; @@ -60,7 +60,7 @@ public function updateCredentials(ActionRequest $actionRequest) */ public function getPassword(): string { - return $this->credentials['password'] ?? ''; + return $this->credentials['password']; } /** diff --git a/Neos.Flow/Classes/Security/Authentication/Token/UsernamePassword.php b/Neos.Flow/Classes/Security/Authentication/Token/UsernamePassword.php index 657ee6627d..5c93accc5e 100644 --- a/Neos.Flow/Classes/Security/Authentication/Token/UsernamePassword.php +++ b/Neos.Flow/Classes/Security/Authentication/Token/UsernamePassword.php @@ -26,7 +26,7 @@ class UsernamePassword extends AbstractToken implements UsernamePasswordTokenInt /** * The username/password credentials - * @var array + * @var array{username: string, password: string,} * @Flow\Transient */ protected $credentials = ['username' => '', 'password' => '']; @@ -78,7 +78,7 @@ public function updateCredentials(ActionRequest $actionRequest) */ public function getUsername(): string { - return $this->credentials['username'] ?? ''; + return $this->credentials['username']; } /** @@ -86,7 +86,7 @@ public function getUsername(): string */ public function getPassword(): string { - return $this->credentials['password'] ?? ''; + return $this->credentials['password']; } /** diff --git a/Neos.Flow/Classes/Security/Authentication/Token/UsernamePasswordHttpBasic.php b/Neos.Flow/Classes/Security/Authentication/Token/UsernamePasswordHttpBasic.php index f8095bae8c..5342185072 100644 --- a/Neos.Flow/Classes/Security/Authentication/Token/UsernamePasswordHttpBasic.php +++ b/Neos.Flow/Classes/Security/Authentication/Token/UsernamePasswordHttpBasic.php @@ -28,7 +28,7 @@ class UsernamePasswordHttpBasic extends UsernamePassword implements SessionlessT */ public function updateCredentials(ActionRequest $actionRequest) { - $this->credentials = ['username' => null, 'password' => null]; + $this->credentials = ['username' => '', 'password' => '']; $this->authenticationStatus = self::NO_CREDENTIALS_GIVEN; $authorizationHeader = $actionRequest->getHttpRequest()->getHeaderLine('Authorization'); diff --git a/Neos.Flow/Classes/Security/Authentication/TokenAndProviderFactory.php b/Neos.Flow/Classes/Security/Authentication/TokenAndProviderFactory.php index 4fe716176e..0314714fe5 100644 --- a/Neos.Flow/Classes/Security/Authentication/TokenAndProviderFactory.php +++ b/Neos.Flow/Classes/Security/Authentication/TokenAndProviderFactory.php @@ -40,7 +40,7 @@ class TokenAndProviderFactory implements TokenAndProviderFactoryInterface protected $tokens = []; /** - * @var array + * @var array> */ protected $providerConfigurations = []; @@ -113,7 +113,7 @@ public function getProviders(): array /** * Inject the settings and does a fresh build of tokens based on the injected settings * - * @param array $settings The settings + * @param array $settings The settings * @return void * @throws Exception */ @@ -151,14 +151,11 @@ protected function buildProvidersAndTokensFromConfiguration() continue; } - if (!is_array($providerConfiguration) || !isset($providerConfiguration['provider'])) { + if (!isset($providerConfiguration['provider'])) { throw new Exception\InvalidAuthenticationProviderException('The configured authentication provider "' . $providerName . '" needs a "provider" option!', 1248209521); } $providerObjectName = $this->providerResolver->resolveProviderClass((string)$providerConfiguration['provider']); - if ($providerObjectName === null) { - throw new Exception\InvalidAuthenticationProviderException('The configured authentication provider "' . $providerConfiguration['provider'] . '" could not be found!', 1237330453); - } $providerOptions = []; if (isset($providerConfiguration['providerOptions']) && is_array($providerConfiguration['providerOptions'])) { $providerOptions = $providerConfiguration['providerOptions']; @@ -175,7 +172,7 @@ protected function buildProvidersAndTokensFromConfiguration() } $tokenInstance = $this->objectManager->get($tokenClassName, $providerConfiguration['tokenOptions'] ?? []); if (!$tokenInstance instanceof TokenInterface) { - throw new Exception\InvalidAuthenticationProviderException(sprintf('The specified token is not an instance of %s but a %s. Please adjust the "token" configuration of the "%s" authentication provider', TokenInterface::class, is_object($tokenInstance) ? get_class($tokenInstance) : gettype($tokenInstance), $providerName), 1585921152); + throw new Exception\InvalidAuthenticationProviderException(sprintf('The specified token is not an instance of %s but a %s. Please adjust the "token" configuration of the "%s" authentication provider', TokenInterface::class, get_class($tokenInstance), $providerName), 1585921152); } $tokenInstance->setAuthenticationProviderName($providerName); $this->tokens[] = $tokenInstance; @@ -225,7 +222,7 @@ protected function buildProvidersAndTokensFromConfiguration() $entryPoint->setOptions($providerConfiguration['entryPointOptions']); } - $tokenInstance->setAuthenticationEntryPoint($entryPoint); + $tokenInstance?->setAuthenticationEntryPoint($entryPoint); } } diff --git a/Neos.Flow/Classes/Security/Authentication/TokenInterface.php b/Neos.Flow/Classes/Security/Authentication/TokenInterface.php index 13b6eb868c..dff63f0388 100644 --- a/Neos.Flow/Classes/Security/Authentication/TokenInterface.php +++ b/Neos.Flow/Classes/Security/Authentication/TokenInterface.php @@ -88,7 +88,7 @@ public function setAuthenticationEntryPoint(EntryPointInterface $entryPoint); /** * Returns the configured authentication entry point, NULL if none is available * - * @return EntryPointInterface The configured authentication entry point, NULL if none is available + * @return ?EntryPointInterface The configured authentication entry point, NULL if none is available */ public function getAuthenticationEntryPoint(); @@ -102,7 +102,7 @@ public function hasRequestPatterns(); /** * Sets request patterns * - * @param array $requestPatterns Array of \Neos\Flow\Security\RequestPatternInterface to be set + * @param array $requestPatterns Array of \Neos\Flow\Security\RequestPatternInterface to be set * @return void * @see hasRequestPattern() */ @@ -139,14 +139,14 @@ public function getCredentials(); /** * Returns the account if one is authenticated, NULL otherwise. * - * @return Account An account object + * @return ?Account An account object */ public function getAccount(); /** * Set the (authenticated) account * - * @param Account $account An account object + * @param ?Account $account An account object * @return void */ public function setAccount(?Account $account = null); diff --git a/Neos.Flow/Classes/Security/Authorization/FilterFirewall.php b/Neos.Flow/Classes/Security/Authorization/FilterFirewall.php index 1d2bc80d53..1798c1e927 100644 --- a/Neos.Flow/Classes/Security/Authorization/FilterFirewall.php +++ b/Neos.Flow/Classes/Security/Authorization/FilterFirewall.php @@ -72,7 +72,7 @@ public function __construct( /** * Injects the configuration settings * - * @param array $settings + * @param array $settings * @return void */ public function injectSettings(array $settings) @@ -101,7 +101,7 @@ public function blockIllegalRequests(ActionRequest $request) } /** - * @param array $filterConfiguration + * @param array $filterConfiguration * @return RequestFilter * @throws \Neos\Flow\Security\Exception\NoInterceptorFoundException * @throws \Neos\Flow\Security\Exception\NoRequestPatternFoundException diff --git a/Neos.Flow/Classes/Security/Authorization/Interceptor/PolicyEnforcement.php b/Neos.Flow/Classes/Security/Authorization/Interceptor/PolicyEnforcement.php index 72a2f0f84c..732c341b35 100644 --- a/Neos.Flow/Classes/Security/Authorization/Interceptor/PolicyEnforcement.php +++ b/Neos.Flow/Classes/Security/Authorization/Interceptor/PolicyEnforcement.php @@ -96,6 +96,7 @@ public function invoke() try { $this->authenticationManager->authenticate(); + /** @phpstan-ignore catch.neverThrown (exception is not part of the api, can occur if the user was deleted) */ } catch (EntityNotFoundException $exception) { throw new AuthenticationRequiredException('Could not authenticate. Looks like a broken session.', 1358971444, $exception); } catch (NoTokensAuthenticatedException $noTokensAuthenticatedException) { diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/ConjunctionGenerator.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/ConjunctionGenerator.php index 6a4d46cdce..56bdbf7aa0 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/ConjunctionGenerator.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/ConjunctionGenerator.php @@ -11,7 +11,7 @@ * source code. */ -use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\Filter\SQLFilter as DoctrineSqlFilter; use Neos\Flow\Annotations as Flow; @@ -35,14 +35,13 @@ public function __construct(array $expressions) /** * @param DoctrineSqlFilter $sqlFilter - * @param ClassMetadata $targetEntity Metadata object for the target entity to create the constraint for + * @param ClassMetadata $targetEntity Metadata object for the target entity to create the constraint for * @param string $targetTableAlias The target table alias used in the current query * @return string */ public function getSql(DoctrineSqlFilter $sqlFilter, ClassMetadata $targetEntity, $targetTableAlias) { $sql = ''; - /** @var SqlGeneratorInterface $expression */ foreach ($this->expressions as $expression) { $sql .= ($sql !== '' ? ' AND ' : '') . $expression->getSql($sqlFilter, $targetEntity, $targetTableAlias); } diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/DisjunctionGenerator.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/DisjunctionGenerator.php index dafb1dfa59..c40043681e 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/DisjunctionGenerator.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/DisjunctionGenerator.php @@ -11,7 +11,7 @@ * source code. */ -use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\Filter\SQLFilter as DoctrineSqlFilter; use Neos\Flow\Annotations as Flow; @@ -35,14 +35,13 @@ public function __construct(array $expressions) /** * @param DoctrineSqlFilter $sqlFilter - * @param ClassMetaData $targetEntity Metadata object for the target entity to create the constraint for + * @param ClassMetaData $targetEntity Metadata object for the target entity to create the constraint for * @param string $targetTableAlias The target table alias used in the current query * @return string */ public function getSql(DoctrineSqlFilter $sqlFilter, ClassMetadata $targetEntity, $targetTableAlias) { $sql = ''; - /** @var SqlGeneratorInterface $expression */ foreach ($this->expressions as $expression) { $sql .= ($sql !== '' ? ' OR ' : '') . $expression->getSql($sqlFilter, $targetEntity, $targetTableAlias); } diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/EntityPrivilege.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/EntityPrivilege.php index 732aa8fade..5274aa6dd3 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/EntityPrivilege.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/EntityPrivilege.php @@ -11,7 +11,6 @@ * source code. */ -use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; use Neos\Eel\Context as EelContext; @@ -60,11 +59,11 @@ public function matchesEntityType($entityType) /** * Note: The result of this method cannot be cached, as the target table alias might change for different query scenarios * - * @param ClassMetadata $targetEntity + * @param \Doctrine\ORM\Mapping\ClassMetadata $targetEntity * @param string $targetTableAlias * @return string|null */ - public function getSqlConstraint(ClassMetadata $targetEntity, $targetTableAlias) + public function getSqlConstraint(\Doctrine\ORM\Mapping\ClassMetadata $targetEntity, $targetTableAlias) { $this->evaluateMatcher(); diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/EntityPrivilegeExpressionParser.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/EntityPrivilegeExpressionParser.php index 4e9bf53e01..b97fafc83e 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/EntityPrivilegeExpressionParser.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/EntityPrivilegeExpressionParser.php @@ -23,10 +23,10 @@ class EntityPrivilegeExpressionParser extends CompilingEelParser { /** - * @param array $result - * @param array $sub + * @param array $result + * @param array $sub */ - public function NotExpression_exp(&$result, $sub) + public function NotExpression_exp(&$result, $sub): void { if (!isset($result['code'])) { $result['code'] = '$context'; @@ -35,29 +35,29 @@ public function NotExpression_exp(&$result, $sub) } /** - * @param array $result - * @param array $sub + * @param array $result + * @param array $sub */ - public function Disjunction_rgt(&$result, $sub) + public function Disjunction_rgt(&$result, $sub): void { $result['code'] = '$context->callAndWrap(\'disjunction\', array(' . $this->unwrapExpression($result['code']) . ', ' . $this->unwrapExpression($sub['code']) . '))'; } /** - * @param array $result - * @param array $sub + * @param array $result + * @param array $sub */ - public function Conjunction_rgt(&$result, $sub) + public function Conjunction_rgt(&$result, $sub): void { $result['code'] = '$context->callAndWrap(\'conjunction\', array(' . $this->unwrapExpression($result['code']) . ', ' . $this->unwrapExpression($sub['code']) . '))'; } /** - * @param array $result - * @param array $sub + * @param array $result + * @param array $sub * @throws ParserException */ - public function Comparison_rgt(&$result, $sub) + public function Comparison_rgt(&$result, $sub): void { $lval = $result['code']; $rval = $sub['code']; diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/FalseConditionGenerator.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/FalseConditionGenerator.php index 6fa1e62d70..abd63bf8ff 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/FalseConditionGenerator.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/FalseConditionGenerator.php @@ -24,7 +24,7 @@ class FalseConditionGenerator implements SqlGeneratorInterface * Returns an SQL query part that is basically a no-op in order to match no entity * * @param DoctrineSqlFilter $sqlFilter - * @param ClassMetadata $targetEntity + * @param ClassMetadata $targetEntity * @param string $targetTableAlias * @return string */ diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/NotExpressionGenerator.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/NotExpressionGenerator.php index 5d52747678..5bbcc1921e 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/NotExpressionGenerator.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/NotExpressionGenerator.php @@ -11,7 +11,7 @@ * source code. */ -use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\Filter\SQLFilter as DoctrineSqlFilter; use Neos\Flow\Annotations as Flow; @@ -35,7 +35,7 @@ public function __construct(SqlGeneratorInterface $expression) /** * @param DoctrineSqlFilter $sqlFilter - * @param ClassMetaData $targetEntity Metadata object for the target entity to create the constraint for + * @param ClassMetaData $targetEntity Metadata object for the target entity to create the constraint for * @param string $targetTableAlias The target table alias used in the current query * @return string */ diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/PropertyConditionGenerator.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/PropertyConditionGenerator.php index 7df14fff9c..0f38d0225a 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/PropertyConditionGenerator.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/PropertyConditionGenerator.php @@ -11,7 +11,7 @@ * source code. */ -use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\QuoteStrategy; @@ -45,7 +45,7 @@ class PropertyConditionGenerator implements SqlGeneratorInterface protected $operator; /** - * @var string|array + * @var string|array */ protected $operandDefinition; @@ -58,7 +58,7 @@ class PropertyConditionGenerator implements SqlGeneratorInterface * Array of registered global objects that can be accessed as operands * * @Flow\InjectConfiguration("aop.globalObjects") - * @var array + * @var array */ protected $globalObjects = []; @@ -95,7 +95,7 @@ class PropertyConditionGenerator implements SqlGeneratorInterface /** * Raw parameter values * - * @var array + * @var array */ protected $parameters = []; @@ -108,7 +108,7 @@ public function __construct($path) } /** - * @param string|array $operandDefinition + * @param string|array $operandDefinition * @return PropertyConditionGenerator the current instance to allow for method chaining */ public function equals($operandDefinition) @@ -120,7 +120,7 @@ public function equals($operandDefinition) } /** - * @param string|array $operandDefinition + * @param string|array $operandDefinition * @return PropertyConditionGenerator the current instance to allow for method chaining */ public function notEquals($operandDefinition) @@ -204,6 +204,9 @@ public function in($operandDefinition) if (is_array($this->operand) === false && ($this->operand instanceof \Traversable) === false) { throw new InvalidPolicyException(sprintf('The "in" operator needs an array as operand! Got: "%s"', $this->operand), 1416313526); } + if (!is_array($this->operandDefinition)) { + throw new \Exception('Cannot assign iterable operands to non-array operand definition', 1743959938); + } foreach ($this->operand as $iterator => $singleOperandValueDefinition) { $this->operandDefinition['inOperandValue' . $iterator] = $singleOperandValueDefinition; } @@ -230,15 +233,16 @@ public function contains($operandDefinition): PropertyConditionGenerator /** * @param DoctrineSqlFilter $sqlFilter - * @param ClassMetadata $targetEntity + * @param ORMClassMetadata $targetEntity * @param string $targetTableAlias * @return string * @throws InvalidQueryRewritingConstraintException * @throws \Exception */ - public function getSql(DoctrineSqlFilter $sqlFilter, ClassMetadata $targetEntity, $targetTableAlias) + public function getSql(DoctrineSqlFilter $sqlFilter, ORMClassMetadata $targetEntity, $targetTableAlias) { - $targetEntityPropertyName = (strpos($this->path, '.') ? substr($this->path, 0, strpos($this->path, '.')) : $this->path); + $pivot = strpos($this->path, '.'); + $targetEntityPropertyName = ($pivot ? substr($this->path, 0, $pivot) : $this->path); $quoteStrategy = $this->entityManager->getConfiguration()->getQuoteStrategy(); if ($targetEntity->hasAssociation($targetEntityPropertyName) === false) { @@ -248,7 +252,11 @@ public function getSql(DoctrineSqlFilter $sqlFilter, ClassMetadata $targetEntity } elseif ($targetEntity->isSingleValuedAssociation($targetEntityPropertyName) === true && $targetEntity->isAssociationInverseSide($targetEntityPropertyName) === false) { return $this->getSqlForManyToOneAndOneToOneRelationsWithPropertyPath($sqlFilter, $quoteStrategy, $targetEntity, $targetTableAlias, $targetEntityPropertyName); } elseif ($targetEntity->isSingleValuedAssociation($targetEntityPropertyName) === true && $targetEntity->isAssociationInverseSide($targetEntityPropertyName) === true) { - throw new InvalidQueryRewritingConstraintException('Single valued properties from the inverse side are not supported in a content security constraint path! Got: "' . $this->path . ' ' . $this->operator . ' ' . $this->operandDefinition . '"', 1416397754); + throw new InvalidQueryRewritingConstraintException( + 'Single valued properties from the inverse side are not supported in a content security constraint path! Got: "' + . $this->path . ' ' . $this->operator . ' ' . json_encode($this->operandDefinition) . '"', + 1416397754 + ); } elseif ($targetEntity->isCollectionValuedAssociation($targetEntityPropertyName) === true) { return $this->getSqlForPropertyContains($sqlFilter, $quoteStrategy, $targetEntity, $targetTableAlias, $targetEntityPropertyName); } @@ -259,17 +267,21 @@ public function getSql(DoctrineSqlFilter $sqlFilter, ClassMetadata $targetEntity /** * @param DoctrineSqlFilter $sqlFilter * @param QuoteStrategy $quoteStrategy - * @param ClassMetadataInfo $targetEntity + * @param ORMClassMetadata $targetEntity * @param string $targetTableAlias * @param string $targetEntityPropertyName * @return string * @throws InvalidQueryRewritingConstraintException * @throws \Exception */ - protected function getSqlForPropertyContains(DoctrineSqlFilter $sqlFilter, QuoteStrategy $quoteStrategy, ClassMetadata $targetEntity, string $targetTableAlias, string $targetEntityPropertyName): string + protected function getSqlForPropertyContains(DoctrineSqlFilter $sqlFilter, QuoteStrategy $quoteStrategy, ORMClassMetadata $targetEntity, string $targetTableAlias, string $targetEntityPropertyName): string { if ($this->operator !== 'contains') { - throw new InvalidQueryRewritingConstraintException('Multivalued properties are not supported in a content security constraint path unless the "contains" operation is used! Got: "' . $this->path . ' ' . $this->operator . ' ' . $this->operandDefinition . '"', 1416397655); + throw new InvalidQueryRewritingConstraintException( + 'Multivalued properties are not supported in a content security constraint path unless the "contains" operation is used! Got: "' + . $this->path . ' ' . $this->operator . ' ' . json_encode($this->operandDefinition) . '"', + 1416397655 + ); } if (is_array($this->operandDefinition)) { @@ -303,9 +315,6 @@ protected function getSqlForPropertyContains(DoctrineSqlFilter $sqlFilter, Quote $subQuerySql = 'SELECT ' . $reverseColumn . ' FROM ' . $associationMapping['joinTable']['name'] . ' WHERE ' . $joinColumn . ' = ' . $parameterValue; } else { $associationTargetClass = $targetEntity->getAssociationTargetClass($targetEntityPropertyName); - if ($associationTargetClass === null) { - throw new \InvalidArgumentException("Association name expected, '" . $targetEntityPropertyName . "' is not an association.", 1629871077); - } $subselectQuery = new Query($associationTargetClass); $rootAliases = $subselectQuery->getQueryBuilder()->getRootAliases(); $primaryRootAlias = reset($rootAliases); @@ -314,6 +323,9 @@ protected function getSqlForPropertyContains(DoctrineSqlFilter $sqlFilter, Quote $subselectQuery->getQueryBuilder()->select('IDENTITY(' . $primaryRootAlias . '.' . $associationMapping['mappedBy'] . ')'); $subQuerySql = $subselectQuery->getSql(); } + if (is_array($subQuerySql)) { + throw new \Exception('invalid sub query, must not be an array', 1743931303); + } return $targetTableAlias . '.' . $identityColumnName . ' IN (' . $subQuerySql . ')'; } @@ -321,14 +333,14 @@ protected function getSqlForPropertyContains(DoctrineSqlFilter $sqlFilter, Quote /** * @param DoctrineSqlFilter $sqlFilter * @param QuoteStrategy $quoteStrategy - * @param ClassMetadata $targetEntity + * @param ORMClassMetadata $targetEntity * @param string $targetTableAlias * @param string $targetEntityPropertyName * @return string * @throws InvalidQueryRewritingConstraintException * @throws \Exception */ - protected function getSqlForSimpleProperty(DoctrineSqlFilter $sqlFilter, QuoteStrategy $quoteStrategy, ClassMetadata $targetEntity, $targetTableAlias, $targetEntityPropertyName) + protected function getSqlForSimpleProperty(DoctrineSqlFilter $sqlFilter, QuoteStrategy $quoteStrategy, ORMClassMetadata $targetEntity, $targetTableAlias, $targetEntityPropertyName) { $quotedColumnName = $quoteStrategy->getColumnName($targetEntityPropertyName, $targetEntity, $this->entityManager->getConnection()->getDatabasePlatform()); $propertyPointer = $targetTableAlias . '.' . $quotedColumnName; @@ -346,35 +358,37 @@ protected function getSqlForSimpleProperty(DoctrineSqlFilter $sqlFilter, QuoteSt /** * @param DoctrineSqlFilter $sqlFilter * @param QuoteStrategy $quoteStrategy - * @param ClassMetadataInfo $targetEntity + * @param ORMClassMetadata $targetEntity * @param string $targetTableAlias * @param string $targetEntityPropertyName * @return string * @throws InvalidQueryRewritingConstraintException * @throws \Exception */ - protected function getSqlForManyToOneAndOneToOneRelationsWithoutPropertyPath(DoctrineSqlFilter $sqlFilter, QuoteStrategy $quoteStrategy, ClassMetadata $targetEntity, $targetTableAlias, $targetEntityPropertyName) + protected function getSqlForManyToOneAndOneToOneRelationsWithoutPropertyPath(DoctrineSqlFilter $sqlFilter, QuoteStrategy $quoteStrategy, ORMClassMetadata $targetEntity, $targetTableAlias, $targetEntityPropertyName) { $associationMapping = $targetEntity->getAssociationMapping($targetEntityPropertyName); $constraints = []; - foreach ($associationMapping['joinColumns'] as $joinColumn) { + foreach ($associationMapping['joinColumns'] ?? [] as $joinColumn) { $quotedColumnName = $quoteStrategy->getJoinColumnName($joinColumn, $targetEntity, $this->entityManager->getConnection()->getDatabasePlatform()); $propertyPointer = $targetTableAlias . '.' . $quotedColumnName; - $operandAlias = $this->operandDefinition; - if (is_array($this->operandDefinition)) { - $operandAlias = key($this->operandDefinition); - } + $operandAlias = is_array($this->operandDefinition) + ? key($this->operandDefinition) + : $this->operandDefinition; + $currentReferencedOperandName = $operandAlias . $joinColumn['referencedColumnName']; if (is_object($this->operand)) { - $operandMetadataInfo = $this->entityManager->getClassMetadata(TypeHandling::getTypeForValue($this->operand)); + $type = TypeHandling::getTypeForValue($this->operand); + $operandMetadataInfo = $this->entityManager->getClassMetadata($type); $currentReferencedValueOfOperand = $operandMetadataInfo->getFieldValue($this->operand, $operandMetadataInfo->getFieldForColumn($joinColumn['referencedColumnName'])); $this->setParameter($sqlFilter, $currentReferencedOperandName, $currentReferencedValueOfOperand, $associationMapping['type']); } elseif (is_array($this->operandDefinition)) { foreach ($this->operandDefinition as $operandIterator => $singleOperandValue) { if (is_object($singleOperandValue)) { - $operandMetadataInfo = $this->entityManager->getClassMetadata(TypeHandling::getTypeForValue($singleOperandValue)); + $type = TypeHandling::getTypeForValue($singleOperandValue); + $operandMetadataInfo = $this->entityManager->getClassMetadata($type); $currentReferencedValueOfOperand = $operandMetadataInfo->getFieldValue($singleOperandValue, $operandMetadataInfo->getFieldForColumn($joinColumn['referencedColumnName'])); $this->setParameter($sqlFilter, $operandIterator, $currentReferencedValueOfOperand, $associationMapping['type']); } elseif ($singleOperandValue === null) { @@ -391,7 +405,7 @@ protected function getSqlForManyToOneAndOneToOneRelationsWithoutPropertyPath(Doc /** * @param DoctrineSqlFilter $sqlFilter * @param QuoteStrategy $quoteStrategy - * @param ClassMetadataInfo $targetEntity + * @param ORMClassMetadata $targetEntity * @param string $targetTableAlias * @param string $targetEntityPropertyName * @return string @@ -405,19 +419,22 @@ protected function getSqlForManyToOneAndOneToOneRelationsWithPropertyPath(Doctri $associationMapping = $targetEntity->getAssociationMapping($targetEntityPropertyName); $subselectConstraintQueries = []; - foreach ($associationMapping['joinColumns'] as $joinColumn) { + foreach ($associationMapping['joinColumns'] ?? [] as $joinColumn) { $rootAliases = $subselectQuery->getQueryBuilder()->getRootAliases(); $subselectQuery->getQueryBuilder()->select($rootAliases[0] . '.' . $targetEntity->getFieldForColumn($joinColumn['referencedColumnName'])); $subselectSql = $subselectQuery->getSql(); + if (is_array($subselectSql)) { + $subselectSql = implode(' ', $subselectSql); + } foreach ($subselectQuery->getParameters() as $parameter) { $parameterValue = $parameter->getValue(); if (is_object($parameterValue)) { $parameterValue = $this->persistenceManager->getIdentifierByObject($parameter->getValue()); } - $subselectSql = preg_replace('/\?/', $this->entityManager->getConnection()->quote($parameterValue, $parameter->getType()), $subselectSql, 1); + $subselectSql = preg_replace('/\?/', $this->entityManager->getConnection()->quote($parameterValue, $parameter->getType()), $subselectSql ?: '', 1); } $quotedColumnName = $quoteStrategy->getJoinColumnName($joinColumn, $targetEntity, $this->entityManager->getConnection()->getDatabasePlatform()); - $subselectIdentifier = 'subselect' . md5($subselectSql); + $subselectIdentifier = 'subselect' . md5($subselectSql ?: ''); $subselectConstraintQueries[] = $targetTableAlias . '.' . $quotedColumnName . ' IN (SELECT ' . $subselectIdentifier . '.' . $joinColumn['referencedColumnName'] . '_0 FROM (' . $subselectSql . ') AS ' . $subselectIdentifier . ' ) '; } @@ -425,7 +442,7 @@ protected function getSqlForManyToOneAndOneToOneRelationsWithPropertyPath(Doctri } /** - * @param ClassMetadata $targetEntity + * @param ClassMetadata $targetEntity * @param string $targetEntityPropertyName * @return Query */ @@ -494,6 +511,9 @@ protected function getConstraintStringForSimpleProperty(DoctrineSqlFilter $sqlFi } $parameter = implode(',', $parameters); } elseif (!($this->getRawParameterValue($operandDefinition) === null || ($this->operator === 'in' && $this->getRawParameterValue($operandDefinition) === []))) { + if (!is_string($operandDefinition)) { + throw new \Exception('SQL filter parameters must be of type string, ' . get_debug_type($operandDefinition) . ' given.', 1743929393); + } $parameter = $sqlFilter->getParameter($operandDefinition); } } catch (\InvalidArgumentException $exception) { @@ -567,11 +587,12 @@ public function getValueForOperand($expression) * @param DoctrineSqlFilter $sqlFilter * @param mixed $name * @param mixed $value - * @param string $type + * @param string|int|null $type * @return void */ protected function setParameter(DoctrineSqlFilter $sqlFilter, $name, $value, $type = null) { + /** @phpstan-ignore argument.type (currently, doctrine also can handle integers) */ $sqlFilter->setParameter($name, $value, $type); $this->parameters[$name] = $value; } diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/SqlFilter.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/SqlFilter.php index e43d3f70d2..f1b3ba9d67 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/SqlFilter.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/SqlFilter.php @@ -40,7 +40,7 @@ class SqlFilter extends DoctrineSqlFilter /** * Gets the SQL query part to add to a query. * - * @param ClassMetadata $targetEntity Metadata object for the target entity to be filtered + * @param ClassMetadata $targetEntity Metadata object for the target entity to be filtered * @param string $targetTableAlias The target table alias used in the current query * @return string The constraint SQL if there is available, empty string otherwise */ @@ -107,6 +107,9 @@ public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAli */ protected function initializeDependencies() { + if (!Bootstrap::$staticObjectManager) { + throw new \Exception('Cannot initialize dependencies without the static object manager', 1744398442); + } if ($this->securityContext === null) { $this->securityContext = Bootstrap::$staticObjectManager->get(Context::class); } diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/SqlGeneratorInterface.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/SqlGeneratorInterface.php index 6f096aec14..c061488576 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/SqlGeneratorInterface.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/SqlGeneratorInterface.php @@ -11,7 +11,7 @@ * source code. */ -use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\Filter\SQLFilter as DoctrineSqlFilter; use Neos\Flow\Annotations as Flow; @@ -22,7 +22,7 @@ interface SqlGeneratorInterface { /** * @param DoctrineSqlFilter $sqlFilter - * @param ClassMetadata $targetEntity Metadata object for the target entity to create the constraint for + * @param ClassMetadata $targetEntity Metadata object for the target entity to create the constraint for * @param string $targetTableAlias The target table alias used in the current query * @return string */ diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/TrueConditionGenerator.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/TrueConditionGenerator.php index 71f5b97f95..68fd7e4e8a 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/TrueConditionGenerator.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/TrueConditionGenerator.php @@ -24,7 +24,7 @@ class TrueConditionGenerator implements SqlGeneratorInterface * Returns an SQL query part that is basically a no-op in order to match any entity * * @param DoctrineSqlFilter $sqlFilter - * @param ClassMetadata $targetEntity + * @param ClassMetadata $targetEntity * @param string $targetTableAlias * @return string */ diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/EntityPrivilegeInterface.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/EntityPrivilegeInterface.php index e9dab06ce2..096207a62b 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/EntityPrivilegeInterface.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Entity/EntityPrivilegeInterface.php @@ -11,7 +11,7 @@ * source code. */ -use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadata; use Neos\Flow\Annotations as Flow; use Neos\Flow\Security\Authorization\Privilege\PrivilegeInterface; @@ -32,9 +32,9 @@ interface EntityPrivilegeInterface extends PrivilegeInterface public function matchesEntityType($entityType); /** - * @param ClassMetadata $targetEntity + * @param ClassMetadata $targetEntity * @param string $targetTableAlias - * @return string + * @return string|null */ public function getSqlConstraint(ClassMetadata $targetEntity, $targetTableAlias); } diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodPrivilege.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodPrivilege.php index 8bed00aa93..aa8d2fb262 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodPrivilege.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodPrivilege.php @@ -27,7 +27,7 @@ class MethodPrivilege extends AbstractPrivilege implements MethodPrivilegeInterface { /** - * @var array + * @var array>> */ protected static $methodPermissions; diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodPrivilegePointcutFilter.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodPrivilegePointcutFilter.php index b0887246b0..1b1766727c 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodPrivilegePointcutFilter.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodPrivilegePointcutFilter.php @@ -34,7 +34,7 @@ class MethodPrivilegePointcutFilter implements PointcutFilterInterface protected $filters = null; /** - * @var array + * @var array */ protected $methodPermissions = []; @@ -83,9 +83,9 @@ public function initializeObject() * Checks if the specified class and method matches against the filter, i.e. if there is a policy entry to intercept this method. * This method also creates a cache entry for every method, to cache the associated roles and privileges. * - * @param string $className Name of the class to check the name of + * @param class-string $className Name of the class to check the name of * @param string $methodName Name of the method to check the name of - * @param string $methodDeclaringClassName Name of the class the method was originally declared in + * @param class-string $methodDeclaringClassName Name of the class the method was originally declared in * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection. * @return boolean true if the names match, otherwise false */ @@ -131,7 +131,7 @@ public function hasRuntimeEvaluationsDefinition(): bool /** * Returns runtime evaluations for the pointcut. * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition(): array { @@ -158,9 +158,9 @@ public function reduceTargetClassNames(ClassNameIndex $classNameIndex): ClassNam } /** - * @param string $className + * @param class-string $className * @param string $methodName - * @param string $methodDeclaringClassName + * @param class-string $methodDeclaringClassName * @param mixed $pointcutQueryIdentifier * @return \Closure */ diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodTargetExpressionParser.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodTargetExpressionParser.php index dec1266201..b079bc44fc 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodTargetExpressionParser.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodTargetExpressionParser.php @@ -30,7 +30,7 @@ class MethodTargetExpressionParser extends PointcutExpressionParser * @param string $operator The operator * @param string $pointcutExpression The pointcut expression (value of the designator) * @param PointcutFilterComposite $pointcutFilterComposite An instance of the pointcut filter composite. The result (ie. the pointcut filter) will be added to this composite object. - * @param array &$trace + * @param array &$trace * @return void * @throws InvalidPointcutExpressionException */ diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Parameter/PrivilegeParameterInterface.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Parameter/PrivilegeParameterInterface.php index 9403192c43..9cc8dfba11 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Parameter/PrivilegeParameterInterface.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Parameter/PrivilegeParameterInterface.php @@ -41,7 +41,7 @@ public function getName(); public function getValue(); /** - * @return array|null + * @return array|null */ public function getPossibleValues(); diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Parameter/StringPrivilegeParameter.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Parameter/StringPrivilegeParameter.php index a099e429b2..1cc91f91d4 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Parameter/StringPrivilegeParameter.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Parameter/StringPrivilegeParameter.php @@ -19,7 +19,7 @@ class StringPrivilegeParameter extends AbstractPrivilegeParameter { /** - * @return array|null + * @return array|null */ public function getPossibleValues() { diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/PrivilegeTarget.php b/Neos.Flow/Classes/Security/Authorization/Privilege/PrivilegeTarget.php index d8b925bce1..42da213bc7 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/PrivilegeTarget.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/PrivilegeTarget.php @@ -121,7 +121,7 @@ public function hasParameters(): bool /** * @param string $permission one of "GRANT", "DENY" or "ABSTAIN" - * @param array $parameters Optional key/value array with parameter names and -values + * @param array $parameters Optional key/value array with parameter names and -values * @return PrivilegeInterface * @throws SecurityException */ @@ -151,7 +151,7 @@ public function getLabel(): string } /** - * @param array $parameters + * @param array $parameters * @return \Closure */ protected function createParameterMapper(array $parameters): \Closure @@ -163,7 +163,7 @@ protected function createParameterMapper(array $parameters): \Closure /** * @param PrivilegeParameterDefinition $parameterDefinition - * @param array $parameters + * @param array $parameters * @return PrivilegeParameterInterface * @throws SecurityException */ diff --git a/Neos.Flow/Classes/Security/Authorization/PrivilegeManager.php b/Neos.Flow/Classes/Security/Authorization/PrivilegeManager.php index 6efd9bb355..1eb0a8a031 100644 --- a/Neos.Flow/Classes/Security/Authorization/PrivilegeManager.php +++ b/Neos.Flow/Classes/Security/Authorization/PrivilegeManager.php @@ -13,6 +13,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\ObjectManagement\ObjectManagerInterface; +use Neos\Flow\Security\Authorization\Privilege\Parameter\PrivilegeParameterInterface; use Neos\Flow\Security\Authorization\Privilege\PrivilegeInterface; use Neos\Flow\Security\Context; use Neos\Flow\Security\Policy\Role; @@ -111,7 +112,7 @@ public function isGrantedForRoles(array $roles, $privilegeType, $subject, &$reas * Returns true if access is granted on the given privilege target in the current security context * * @param string $privilegeTargetIdentifier The identifier of the privilege target to decide on - * @param array $privilegeParameters Optional array of privilege parameters (simple key => value array) + * @param array $privilegeParameters Optional array of privilege parameters (simple key => value array) * @return boolean true if access is granted, false otherwise */ public function isPrivilegeTargetGranted($privilegeTargetIdentifier, array $privilegeParameters = []) @@ -124,7 +125,7 @@ public function isPrivilegeTargetGranted($privilegeTargetIdentifier, array $priv * * @param array $roles The roles that should be evaluated * @param string $privilegeTargetIdentifier The identifier of the privilege target to decide on - * @param array $privilegeParameters Optional array of privilege parameters (simple key => value array) + * @param array $privilegeParameters Optional array of privilege parameters (simple key => value array) * @return boolean true if access is granted, false otherwise */ public function isPrivilegeTargetGrantedForRoles(array $roles, $privilegeTargetIdentifier, array $privilegeParameters = []) @@ -134,7 +135,6 @@ public function isPrivilegeTargetGrantedForRoles(array $roles, $privilegeTargetI }; $privileges = array_map($privilegeMapper, $roles); - /** @var PrivilegePermissionResult $result */ $result = array_reduce($privileges, [$this, 'applyPrivilegeToResult'], new PrivilegePermissionResult()); if ($result->getDenies() === 0 && $result->getGrants() > 0) { diff --git a/Neos.Flow/Classes/Security/Authorization/PrivilegeManagerInterface.php b/Neos.Flow/Classes/Security/Authorization/PrivilegeManagerInterface.php index da5b738c16..2f2ef9dde4 100644 --- a/Neos.Flow/Classes/Security/Authorization/PrivilegeManagerInterface.php +++ b/Neos.Flow/Classes/Security/Authorization/PrivilegeManagerInterface.php @@ -44,7 +44,7 @@ public function isGrantedForRoles(array $roles, $privilegeType, $subject, &$reas * Returns true if access is granted on the given privilege target in the current security context * * @param string $privilegeTargetIdentifier The identifier of the privilege target to decide on - * @param array $privilegeParameters Optional array of privilege parameters (simple key => value array) + * @param array $privilegeParameters Optional array of privilege parameters (simple key => value array) * @return boolean true if access is granted, false otherwise */ public function isPrivilegeTargetGranted($privilegeTargetIdentifier, array $privilegeParameters = []); @@ -54,7 +54,7 @@ public function isPrivilegeTargetGranted($privilegeTargetIdentifier, array $priv * * @param array $roles The roles that should be evaluated * @param string $privilegeTargetIdentifier The identifier of the privilege target to decide on - * @param array $privilegeParameters Optional array of privilege parameters (simple key => value array) + * @param array $privilegeParameters Optional array of privilege parameters (simple key => value array) * @return boolean true if access is granted, false otherwise */ public function isPrivilegeTargetGrantedForRoles(array $roles, $privilegeTargetIdentifier, array $privilegeParameters = []); diff --git a/Neos.Flow/Classes/Security/Authorization/PrivilegePermissionResult.php b/Neos.Flow/Classes/Security/Authorization/PrivilegePermissionResult.php index 92eabf6397..5cb35aea57 100644 --- a/Neos.Flow/Classes/Security/Authorization/PrivilegePermissionResult.php +++ b/Neos.Flow/Classes/Security/Authorization/PrivilegePermissionResult.php @@ -25,7 +25,7 @@ class PrivilegePermissionResult protected $abstains = 0; /** - * @var array + * @var array */ protected $effectivePrivilegeIdentifiersWithPermission = []; @@ -101,7 +101,7 @@ public function getAbstains(): int } /** - * @return array + * @return array */ public function getEffectivePrivilegeIdentifiersWithPermission(): array { diff --git a/Neos.Flow/Classes/Security/Authorization/TestingPrivilegeManager.php b/Neos.Flow/Classes/Security/Authorization/TestingPrivilegeManager.php index ef758473b7..696b1c6bb3 100644 --- a/Neos.Flow/Classes/Security/Authorization/TestingPrivilegeManager.php +++ b/Neos.Flow/Classes/Security/Authorization/TestingPrivilegeManager.php @@ -51,7 +51,7 @@ public function isGranted($privilegeType, $subject, &$reason = '') * or if set based on the override decision value. * * @param string $privilegeTargetIdentifier The identifier of the privilege target to decide on - * @param array $privilegeParameters Optional array of privilege parameters (simple key => value array) + * @param array $privilegeParameters Optional array of privilege parameters (simple key => value array) * @return boolean true if access is granted, false otherwise */ public function isPrivilegeTargetGranted($privilegeTargetIdentifier, array $privilegeParameters = []) diff --git a/Neos.Flow/Classes/Security/Context.php b/Neos.Flow/Classes/Security/Context.php index 8726a4265e..f136872125 100644 --- a/Neos.Flow/Classes/Security/Context.php +++ b/Neos.Flow/Classes/Security/Context.php @@ -95,7 +95,7 @@ class Context protected $csrfProtectionStrategy = self::CSRF_ONE_PER_SESSION; /** - * @var array + * @var array */ protected $tokenStatusLabels = [ 1 => 'no credentials given', @@ -149,7 +149,7 @@ class Context /** * CSRF tokens that are valid during this request but will be gone after. - * @var array + * @var array */ protected $csrfTokensRemovedAfterCurrentRequest = []; @@ -193,7 +193,7 @@ class Context * Array of registered global objects that can be accessed as operands * * @Flow\InjectConfiguration("aop.globalObjects") - * @var array + * @var array */ protected $globalObjects = []; @@ -253,7 +253,7 @@ public function setRequest(ActionRequest $request) /** * Injects the configuration settings * - * @param array $settings + * @param array $settings * @return void * @throws Exception */ @@ -624,7 +624,7 @@ public function clearContext() /** * @param Account $account - * @return array + * @return array */ protected function collectRolesAndParentRolesFromAccount(Account $account): array { @@ -640,7 +640,7 @@ protected function collectRolesAndParentRolesFromAccount(Account $account): arra /** * @param Role $role - * @return array + * @return array */ protected function collectParentRoles(Role $role): array { @@ -692,6 +692,9 @@ protected function isTokenActive(TokenInterface $token) if (isset($requestPatternsByType[$patternType]) && $requestPatternsByType[$patternType] === true) { continue; } + if (!$this->request) { + throw new \Exception('Cannot evaluate request pattern without a request', 1743927165); + } $requestPatternsByType[$patternType] = $requestPattern->matchRequest($this->request); } return !in_array(false, $requestPatternsByType, true); @@ -704,7 +707,7 @@ protected function isTokenActive(TokenInterface $token) * * @param array $managerTokens Array of tokens provided by the authentication manager * @param array $sessionTokens Array of tokens restored from the session - * @return array Array of Authentication\TokenInterface objects + * @return array Array of Authentication\TokenInterface objects */ protected function mergeTokens(array $managerTokens, array $sessionTokens) { @@ -848,6 +851,7 @@ public function getContextHash() $this->withoutAuthorizationChecks(function () use (&$contextHashSoFar) { foreach ($this->globalObjects as $globalObjectsRegisteredClassName) { if (is_subclass_of($globalObjectsRegisteredClassName, CacheAwareInterface::class)) { + /** @var class-string $globalObjectsRegisteredClassName */ $globalObject = $this->objectManager->get($globalObjectsRegisteredClassName); $contextHashSoFar .= '<' . $globalObject->getCacheEntryIdentifier(); } diff --git a/Neos.Flow/Classes/Security/Cryptography/Algorithms.php b/Neos.Flow/Classes/Security/Cryptography/Algorithms.php index f0d1f0c969..20e2e90898 100644 --- a/Neos.Flow/Classes/Security/Cryptography/Algorithms.php +++ b/Neos.Flow/Classes/Security/Cryptography/Algorithms.php @@ -32,6 +32,15 @@ class Algorithms */ public static function pbkdf2($password, $salt, $iterationCount, $derivedKeyLength, $algorithm = 'sha256') { + if (!$algorithm) { + throw new \Exception('Algorithm must not be falsy', 1743877215); + } + if ($iterationCount < 1) { + throw new \Exception('Iteration count too low, must be at least 1', 1743877176); + } + if ($derivedKeyLength < 0) { + throw new \Exception('Derived key length too low, must be at least 0', 1743877275); + } return hash_pbkdf2($algorithm, $password, $salt, $iterationCount, $derivedKeyLength, true); } } diff --git a/Neos.Flow/Classes/Security/Cryptography/FileBasedSimpleKeyService.php b/Neos.Flow/Classes/Security/Cryptography/FileBasedSimpleKeyService.php index a2897353bd..2c9f5876a0 100644 --- a/Neos.Flow/Classes/Security/Cryptography/FileBasedSimpleKeyService.php +++ b/Neos.Flow/Classes/Security/Cryptography/FileBasedSimpleKeyService.php @@ -45,7 +45,7 @@ class FileBasedSimpleKeyService protected $hashService; /** - * @param array $settings + * @param array $settings * @return void */ public function injectSettings(array $settings) @@ -116,7 +116,7 @@ public function keyExists($name) * Returns a key by its name * * @param string $name - * @return boolean + * @return string * @throws SecurityException */ public function getKey($name) diff --git a/Neos.Flow/Classes/Security/Cryptography/HashService.php b/Neos.Flow/Classes/Security/Cryptography/HashService.php index 24df1e3af7..59f567d405 100644 --- a/Neos.Flow/Classes/Security/Cryptography/HashService.php +++ b/Neos.Flow/Classes/Security/Cryptography/HashService.php @@ -28,17 +28,17 @@ class HashService { /** * A private, unique key used for encryption tasks - * @var string + * @var string|false|null */ protected $encryptionKey = null; /** - * @var array + * @var array */ protected $passwordHashingStrategies = []; /** - * @var array + * @var array */ protected $strategySettings; @@ -57,7 +57,7 @@ class HashService /** * Injects the settings of the package this controller belongs to. * - * @param array $settings Settings container of the current package + * @param array $settings Settings container of the current package * @return void */ public function injectSettings(array $settings) @@ -72,12 +72,8 @@ public function injectSettings(array $settings) * @return string The hash of the string * @throws InvalidArgumentForHashGenerationException if something else than a string was given as parameter */ - public function generateHmac($string) + public function generateHmac(string $string): string { - if (!is_string($string)) { - throw new InvalidArgumentForHashGenerationException('A hash can only be generated for a string, but "' . gettype($string) . '" was given.', 1255069587); - } - return hash_hmac('sha1', $string, $this->getEncryptionKey()); } @@ -121,11 +117,8 @@ public function validateHmac($string, $hmac) * @throws InvalidHashException if the hash did not fit to the data. * @todo Mark as API once it is more stable */ - public function validateAndStripHmac($string) + public function validateAndStripHmac(string $string): string { - if (!is_string($string)) { - throw new InvalidArgumentForHashGenerationException('A hash can only be validated for a string, but "' . gettype($string) . '" was given.', 1320829762); - } if (strlen($string) < 40) { throw new InvalidArgumentForHashGenerationException('A hashed string must contain at least 40 characters, the given string was only ' . strlen($string) . ' characters long.', 1320830276); } @@ -188,12 +181,19 @@ protected function getPasswordHashingStrategyAndIdentifier($strategyIdentifier = } $strategyIdentifier = $this->strategySettings['default']; } + if (!is_string($strategyIdentifier)) { + throw new \Exception('Invalid strategy identifier', 1743877483); + } if (!isset($this->strategySettings[$strategyIdentifier])) { throw new MissingConfigurationException('No hashing strategy with identifier "' . $strategyIdentifier . '" configured', 1320758776); } $strategyObjectName = $this->strategySettings[$strategyIdentifier]; - $this->passwordHashingStrategies[$strategyIdentifier] = $this->objectManager->get($strategyObjectName); + $strategy = $this->objectManager->get($strategyObjectName); + if (!$strategy instanceof PasswordHashingStrategyInterface) { + throw new \Exception('Invalid password hashing strategy', 1743877416); + } + $this->passwordHashingStrategies[$strategyIdentifier] = $strategy; return [$this->passwordHashingStrategies[$strategyIdentifier], $strategyIdentifier]; } diff --git a/Neos.Flow/Classes/Security/Cryptography/Pbkdf2HashingStrategy.php b/Neos.Flow/Classes/Security/Cryptography/Pbkdf2HashingStrategy.php index e75a985c66..940f61c21c 100644 --- a/Neos.Flow/Classes/Security/Cryptography/Pbkdf2HashingStrategy.php +++ b/Neos.Flow/Classes/Security/Cryptography/Pbkdf2HashingStrategy.php @@ -68,6 +68,15 @@ public function __construct(int $dynamicSaltLength, int $iterationCount, int $de public function hashPassword($password, $staticSalt = null) { $dynamicSalt = UtilityAlgorithms::generateRandomBytes($this->dynamicSaltLength); + if (!$this->algorithm) { + throw new \Exception('Algorithm must not be falsy', 1743877215); + } + if ($this->iterationCount < 1) { + throw new \Exception('Iteration count too low, must be at least 1', 1743877176); + } + if ($this->derivedKeyLength < 0) { + throw new \Exception('Derived key length too low, must be at least 0', 1743877275); + } $result = hash_pbkdf2($this->algorithm, $password, $dynamicSalt . $staticSalt, $this->iterationCount, $this->derivedKeyLength, true); return base64_encode($dynamicSalt) . ',' . base64_encode($result); } @@ -88,6 +97,12 @@ public function validatePassword($password, $hashedPasswordAndSalt, $staticSalt if (count($parts) !== 2) { throw new \InvalidArgumentException('The derived key with salt must contain a salt, separated with a comma from the derived key', 1306172911); } + if (!$this->algorithm) { + throw new \Exception('Algorithm must not be falsy', 1743877215); + } + if ($this->iterationCount < 1) { + throw new \Exception('Iteration count too low, must be at least 1', 1743877176); + } $dynamicSalt = base64_decode($parts[0]); $derivedKey = base64_decode($parts[1]); $derivedKeyLength = strlen($derivedKey); diff --git a/Neos.Flow/Classes/Security/Cryptography/RsaWalletServiceInterface.php b/Neos.Flow/Classes/Security/Cryptography/RsaWalletServiceInterface.php index 8803d39061..c839393a1e 100644 --- a/Neos.Flow/Classes/Security/Cryptography/RsaWalletServiceInterface.php +++ b/Neos.Flow/Classes/Security/Cryptography/RsaWalletServiceInterface.php @@ -54,7 +54,7 @@ public function registerPublicKeyFromString($publicKeyString); * @return OpenSslRsaKey The public key * @throws InvalidKeyPairIdException If the given fingerprint identifies no valid key pair */ - public function getPublicKey($fingerprint); + public function getPublicKey(string $fingerprint): OpenSslRsaKey; /** * Decrypts the given cypher with the private key identified by the given fingerprint @@ -67,7 +67,7 @@ public function getPublicKey($fingerprint); * @throws InvalidKeyPairIdException If the given fingerprint identifies no valid keypair * @throws DecryptionNotAllowedException If the given fingerprint identifies a keypair for encrypted passwords */ - public function decrypt($cypher, $fingerprint); + public function decrypt(string $cypher, string $fingerprint): string; /** * Signs the given plaintext with the private key identified by the given fingerprint @@ -77,7 +77,7 @@ public function decrypt($cypher, $fingerprint); * @return string The signature of the given plaintext * @throws InvalidKeyPairIdException If the given fingerprint identifies no valid keypair */ - public function sign($plaintext, $fingerprint); + public function sign(string $plaintext, string $fingerprint): string; /** * Checks whether the given signature is valid for the given plaintext @@ -88,7 +88,7 @@ public function sign($plaintext, $fingerprint); * @param string $fingerprint The fingerprint to identify to correct public key * @return boolean true if the signature is correct for the given plaintext and public key */ - public function verifySignature($plaintext, $signature, $fingerprint); + public function verifySignature(string $plaintext, string $signature, string $fingerprint): bool; /** * Encrypts the given plaintext with the public key identified by the given fingerprint @@ -109,14 +109,12 @@ public function encryptWithPublicKey($plaintext, $fingerprint); * @param string $fingerprint The fingerprint to identify to correct private key * @return boolean true if the password is correct */ - public function checkRSAEncryptedPassword($encryptedPassword, $passwordHash, $salt, $fingerprint); + public function checkRSAEncryptedPassword(string $encryptedPassword, string $passwordHash, string $salt, string $fingerprint): bool; /** * Destroys the keypair identified by the given fingerprint * - * @param string $fingerprint The fingerprint - * @return void * @throws InvalidKeyPairIdException If the given fingerprint identifies no valid key pair */ - public function destroyKeypair($fingerprint); + public function destroyKeypair(string $fingerprint): void; } diff --git a/Neos.Flow/Classes/Security/Cryptography/RsaWalletServicePhp.php b/Neos.Flow/Classes/Security/Cryptography/RsaWalletServicePhp.php index e861eb01ea..e302021639 100644 --- a/Neos.Flow/Classes/Security/Cryptography/RsaWalletServicePhp.php +++ b/Neos.Flow/Classes/Security/Cryptography/RsaWalletServicePhp.php @@ -31,13 +31,13 @@ class RsaWalletServicePhp implements RsaWalletServiceInterface protected $keystorePathAndFilename; /** - * @var array + * @var array */ protected $keys = []; /** * The openSSL configuration - * @var array + * @var array */ protected $openSSLConfiguration = []; @@ -55,7 +55,7 @@ class RsaWalletServicePhp implements RsaWalletServiceInterface /** * Injects the OpenSSL configuration to be used * - * @param array $settings + * @param array $settings * @return void * @throws MissingConfigurationException * @throws SecurityException @@ -88,7 +88,7 @@ public function injectSettings(array $settings) public function initializeObject() { if (file_exists($this->keystorePathAndFilename)) { - $this->keys = unserialize(file_get_contents($this->keystorePathAndFilename), ['allowed_classes' => [OpenSslRsaKey::class]]); + $this->keys = unserialize(file_get_contents($this->keystorePathAndFilename) ?: '', ['allowed_classes' => [OpenSslRsaKey::class]]); } $this->saveKeysOnShutdown = false; } @@ -128,6 +128,9 @@ public function generateNewKeypair($usedForPasswords = false) public function registerKeyPairFromPrivateKeyString($privateKeyString, $usedForPasswords = false) { $keyResource = openssl_pkey_get_private($privateKeyString); + if ($keyResource === false) { + throw new \Exception('Failed to load private key', 1743877014); + } $modulus = $this->getModulus($keyResource); $publicKeyString = $this->getPublicKeyString($keyResource); @@ -149,6 +152,9 @@ public function registerKeyPairFromPrivateKeyString($privateKeyString, $usedForP public function registerPublicKeyFromString($publicKeyString) { $keyResource = openssl_pkey_get_public($publicKeyString); + if ($keyResource === false) { + throw new \Exception('Failed to load public key', 1743876948); + } $modulus = $this->getModulus($keyResource); $publicKey = new OpenSslRsaKey($modulus, $publicKeyString); @@ -163,9 +169,9 @@ public function registerPublicKeyFromString($publicKeyString) * @return OpenSslRsaKey The public key * @throws InvalidKeyPairIdException If the given fingerprint identifies no valid key pair */ - public function getPublicKey($fingerprint) + public function getPublicKey(string $fingerprint): OpenSslRsaKey { - if ($fingerprint === null || !isset($this->keys[$fingerprint])) { + if (!isset($this->keys[$fingerprint])) { throw new InvalidKeyPairIdException('Invalid keypair fingerprint given', 1231438860); } @@ -199,16 +205,16 @@ public function encryptWithPublicKey($plaintext, $fingerprint) * Note: You should never decrypt a password with this function. Use checkRSAEncryptedPassword() * to check passwords! * - * @param string $cipher cipher text to decrypt + * @param string $cypher cipher text to decrypt * @param string $fingerprint The fingerprint to identify the private key (RSA public key fingerprint) * @return string The decrypted text * @throws InvalidKeyPairIdException If the given fingerprint identifies no valid keypair * @throws DecryptionNotAllowedException If the given fingerprint identifies a keypair for encrypted passwords * @throws SecurityException If decryption failed for some other reason */ - public function decrypt($cipher, $fingerprint) + public function decrypt(string $cypher, string $fingerprint): string { - if ($fingerprint === null || !isset($this->keys[$fingerprint])) { + if (!isset($this->keys[$fingerprint])) { throw new InvalidKeyPairIdException('Invalid keypair fingerprint given', 1231438861); } @@ -218,7 +224,7 @@ public function decrypt($cipher, $fingerprint) throw new DecryptionNotAllowedException('You are not allowed to decrypt passwords!', 1233655350); } - return $this->decryptWithPrivateKey($cipher, $keyPair['privateKey']); + return $this->decryptWithPrivateKey($cypher, $keyPair['privateKey']); } /** @@ -229,9 +235,9 @@ public function decrypt($cipher, $fingerprint) * @return string The signature of the given plaintext * @throws InvalidKeyPairIdException If the given fingerprint identifies no valid keypair */ - public function sign($plaintext, $fingerprint) + public function sign(string $plaintext, string $fingerprint): string { - if ($fingerprint === null || !isset($this->keys[$fingerprint])) { + if (!isset($this->keys[$fingerprint])) { throw new InvalidKeyPairIdException('Invalid keypair fingerprint given', 1299095799); } @@ -251,9 +257,9 @@ public function sign($plaintext, $fingerprint) * @return boolean true if the signature is correct for the given plaintext and public key * @throws InvalidKeyPairIdException */ - public function verifySignature($plaintext, $signature, $fingerprint) + public function verifySignature(string $plaintext, string $signature, string $fingerprint): bool { - if ($fingerprint === null || !isset($this->keys[$fingerprint])) { + if (!isset($this->keys[$fingerprint])) { throw new InvalidKeyPairIdException('Invalid keypair fingerprint given', 1304959763); } @@ -273,9 +279,9 @@ public function verifySignature($plaintext, $signature, $fingerprint) * @return boolean true if the password is correct * @throws InvalidKeyPairIdException If the given fingerprint identifies no valid keypair */ - public function checkRSAEncryptedPassword($encryptedPassword, $passwordHash, $salt, $fingerprint) + public function checkRSAEncryptedPassword(string $encryptedPassword, string $passwordHash, string $salt, string $fingerprint): bool { - if ($fingerprint === null || !isset($this->keys[$fingerprint])) { + if (!isset($this->keys[$fingerprint])) { throw new InvalidKeyPairIdException('Invalid keypair fingerprint given', 1233655216); } @@ -288,12 +294,11 @@ public function checkRSAEncryptedPassword($encryptedPassword, $passwordHash, $sa * Destroys the keypair identified by the given fingerprint * * @param string $fingerprint The fingerprint - * @return void * @throws InvalidKeyPairIdException If the given fingerprint identifies no valid key pair */ - public function destroyKeypair($fingerprint) + public function destroyKeypair(string $fingerprint): void { - if ($fingerprint === null || !isset($this->keys[$fingerprint])) { + if (!isset($this->keys[$fingerprint])) { throw new InvalidKeyPairIdException('Invalid keypair fingerprint given', 1231438863); } @@ -322,6 +327,9 @@ private function getPrivateKeyString(\OpenSSLAsymmetricKey $keyResource) private function getPublicKeyString(\OpenSSLAsymmetricKey $keyResource) { $keyDetails = openssl_pkey_get_details($keyResource); + if ($keyDetails === false) { + throw new \Exception('Failed to get public key details', 1743876036); + } return $keyDetails['key']; } @@ -335,6 +343,9 @@ private function getPublicKeyString(\OpenSSLAsymmetricKey $keyResource) private function getModulus(\OpenSSLAsymmetricKey $keyResource) { $keyDetails = openssl_pkey_get_details($keyResource); + if ($keyDetails === false) { + throw new \Exception('Failed to load public key details', 1743875916); + } return strtoupper(bin2hex($keyDetails['rsa']['n'])); } @@ -350,6 +361,9 @@ private function decryptWithPrivateKey($cipher, OpenSslRsaKey $privateKey) { $decrypted = ''; $key = openssl_pkey_get_private($privateKey->getKeyString()); + if ($key === false) { + throw new \Exception('Failed to load private key', 1743875855); + } if (openssl_private_decrypt($cipher, $decrypted, $key, $this->paddingAlgorithm) === false) { // Fallback for data that was encrypted with old default OPENSSL_PKCS1_PADDING if ($this->paddingAlgorithm !== OPENSSL_PKCS1_PADDING) { @@ -374,7 +388,7 @@ private function decryptWithPrivateKey($cipher, OpenSslRsaKey $privateKey) * consistent key access. * * @param OpenSslRsaKey $publicKey The public key - * @param OpenSslRsaKey $privateKey The private key + * @param OpenSslRsaKey|null $privateKey The private key * @param boolean $usedForPasswords true if this keypair should be used to encrypt passwords (then decryption won't be allowed!). * @return string The fingerprint which is used as an identifier for storing the key pair */ @@ -439,7 +453,13 @@ public function shutdownObject() public function getFingerprintByPublicKey($publicKeyString) { $keyResource = openssl_pkey_get_public($publicKeyString); + if ($keyResource === false) { + throw new \Exception('Could not extract public key', 1743875618); + } $keyDetails = openssl_pkey_get_details($keyResource); + if ($keyDetails === false) { + throw new \Exception('Could not extract public key details', 1743875650); + } $modulus = $this->sshConvertMpint($keyDetails['rsa']['n']); $publicExponent = $this->sshConvertMpint($keyDetails['rsa']['e']); diff --git a/Neos.Flow/Classes/Security/DummyContext.php b/Neos.Flow/Classes/Security/DummyContext.php index a9689677ef..57474421d9 100644 --- a/Neos.Flow/Classes/Security/DummyContext.php +++ b/Neos.Flow/Classes/Security/DummyContext.php @@ -35,7 +35,7 @@ class DummyContext extends Context /** * Array of configured tokens (might have request patterns) - * @var array + * @var array */ protected $tokens = []; @@ -86,7 +86,7 @@ public function getAuthenticationStrategy() * Sets the Authentication\Tokens of the security context which should be active. * * @param TokenInterface[] $tokens Array of set tokens - * @return array + * @return array */ public function setAuthenticationTokens(array $tokens) { @@ -110,7 +110,7 @@ public function getAuthenticationTokens() * active for the current request and of the given type. If a token has a request pattern that cannot match * against the current request it is determined as not active. * - * @param string $className The class name + * @param class-string $className The class name * @return TokenInterface[] Array of set tokens of the specified type */ public function getAuthenticationTokensOfType($className) @@ -132,7 +132,7 @@ public function getAuthenticationTokensOfType($className) * * The "Neos.Flow:Everybody" roles is always returned. * - * @return Role[] + * @return Role[]|null */ public function getRoles() { @@ -185,7 +185,7 @@ public function setCsrfProtectionToken($csrfProtectionToken) * Returns the current CSRF protection token. A new one is created when needed, depending on the configured CSRF * protection strategy. * - * @return string + * @return string|null */ public function getCsrfProtectionToken() { @@ -231,7 +231,7 @@ public function setInterceptedRequest(?ActionRequest $interceptedRequest = null) * Returns the request, that has been stored for later resuming after it * has been intercepted by a security exception, NULL if there is none. * - * @return ActionRequest + * @return ActionRequest|null */ public function getInterceptedRequest() { diff --git a/Neos.Flow/Classes/Security/Policy/PolicyService.php b/Neos.Flow/Classes/Security/Policy/PolicyService.php index f2e8228ccb..aab68aa904 100644 --- a/Neos.Flow/Classes/Security/Policy/PolicyService.php +++ b/Neos.Flow/Classes/Security/Policy/PolicyService.php @@ -46,7 +46,7 @@ class PolicyService protected $configurationManager; /** - * @var array + * @var array */ protected $policyConfiguration; @@ -198,7 +198,7 @@ protected function initializePrivilegeTargets(): void if ($parameterClassName === null) { throw new SecurityException(sprintf('No "className" defined for parameter "%s" in privilegeTarget "%s"', $parameterName, $privilegeTargetIdentifier), 1396021782); } - if (!in_array(PrivilegeParameterInterface::class, class_implements($parameterClassName), true)) { + if (!in_array(PrivilegeParameterInterface::class, class_implements($parameterClassName) ?: [], true)) { throw new SecurityException(sprintf('PrivilegeParameterInterface must be implemented for "className" defined for parameter "%s" in privilegeTarget "%s"', $parameterName, $privilegeTargetIdentifier), 1396021782); } $parameterDefinitions[$parameterName] = new PrivilegeParameterDefinition($parameterName, $parameterClassName); @@ -266,7 +266,7 @@ public function getRoles($includeAbstract = false): array * Returns all privileges of the given type * * @param string $type Full qualified class or interface name - * @return array + * @return array * @throws InvalidConfigurationTypeException * @throws SecurityException */ @@ -325,7 +325,7 @@ public function reset(): void * This signal can be used to add roles and/or privilegeTargets during runtime. In the slot make sure to receive the * $policyConfiguration array by reference so you can alter it. * - * @param array $policyConfiguration The policy configuration + * @param array $policyConfiguration The policy configuration * @return void * @Flow\Signal */ diff --git a/Neos.Flow/Classes/Security/Policy/Role.php b/Neos.Flow/Classes/Security/Policy/Role.php index e4ae2790ac..b1e5968140 100644 --- a/Neos.Flow/Classes/Security/Policy/Role.php +++ b/Neos.Flow/Classes/Security/Policy/Role.php @@ -14,6 +14,7 @@ */ use Neos\Flow\Annotations as Flow; +use Neos\Flow\Security\Authorization\Privilege\Parameter\PrivilegeParameterInterface; use Neos\Flow\Security\Authorization\Privilege\PrivilegeInterface; /** @@ -246,7 +247,7 @@ public function getPrivilegesByType(string $className): array /** * @param string $privilegeTargetIdentifier - * @param array $privilegeParameters + * @param array $privilegeParameters * @return PrivilegeInterface|null the matching privilege or NULL if no privilege exists for the given constraints */ public function getPrivilegeForTarget(string $privilegeTargetIdentifier, array $privilegeParameters = []): ?PrivilegeInterface diff --git a/Neos.Flow/Classes/Security/Policy/RoleConverter.php b/Neos.Flow/Classes/Security/Policy/RoleConverter.php index bd599cb85d..b90c842404 100644 --- a/Neos.Flow/Classes/Security/Policy/RoleConverter.php +++ b/Neos.Flow/Classes/Security/Policy/RoleConverter.php @@ -52,7 +52,7 @@ class RoleConverter extends AbstractTypeConverter * * @param mixed $source * @param string $targetType - * @param array $convertedChildProperties + * @param array $convertedChildProperties * @param PropertyMappingConfigurationInterface|null $configuration * @return object the target type */ diff --git a/Neos.Flow/Classes/Security/RequestPattern/ControllerObjectName.php b/Neos.Flow/Classes/Security/RequestPattern/ControllerObjectName.php index b95619530c..9c61769698 100644 --- a/Neos.Flow/Classes/Security/RequestPattern/ControllerObjectName.php +++ b/Neos.Flow/Classes/Security/RequestPattern/ControllerObjectName.php @@ -21,14 +21,14 @@ class ControllerObjectName implements RequestPatternInterface { /** - * @var array + * @var array */ protected $options; /** * Expects options in the form array('controllerObjectNamePattern' => '') * - * @param array $options + * @param array $options */ public function __construct(array $options) { diff --git a/Neos.Flow/Classes/Security/RequestPattern/CsrfProtection.php b/Neos.Flow/Classes/Security/RequestPattern/CsrfProtection.php index ffa2eef7e2..f7a7bc8922 100644 --- a/Neos.Flow/Classes/Security/RequestPattern/CsrfProtection.php +++ b/Neos.Flow/Classes/Security/RequestPattern/CsrfProtection.php @@ -101,6 +101,9 @@ public function matchRequest(ActionRequest $request) } $controllerClassName = $this->objectManager->getClassNameByObjectName($request->getControllerObjectName()); + if ($controllerClassName === false) { + throw new \Exception('Failed to resolve controller class name for ' . $request->getControllerObjectName(), 1743926779); + } $actionMethodName = $request->getControllerActionName() . 'Action'; if (!$this->hasPolicyEntryForMethod($controllerClassName, $actionMethodName)) { diff --git a/Neos.Flow/Classes/Security/RequestPattern/Host.php b/Neos.Flow/Classes/Security/RequestPattern/Host.php index 1b1c1a00a9..3265905e68 100644 --- a/Neos.Flow/Classes/Security/RequestPattern/Host.php +++ b/Neos.Flow/Classes/Security/RequestPattern/Host.php @@ -25,14 +25,14 @@ class Host implements RequestPatternInterface { /** - * @var array + * @var array */ protected $options; /** * Expects options in the form array('hostPattern' => '') * - * @param array $options + * @param array $options */ public function __construct(array $options) { diff --git a/Neos.Flow/Classes/Security/RequestPattern/Ip.php b/Neos.Flow/Classes/Security/RequestPattern/Ip.php index 18cf903ad5..02358ebf46 100644 --- a/Neos.Flow/Classes/Security/RequestPattern/Ip.php +++ b/Neos.Flow/Classes/Security/RequestPattern/Ip.php @@ -35,14 +35,14 @@ class Ip implements RequestPatternInterface { /** - * @var array + * @var array */ protected $options; /** * Expects options in the form array('cidrPattern' => '') * - * @param array $options + * @param array $options */ public function __construct(array $options) { diff --git a/Neos.Flow/Classes/Security/RequestPattern/Uri.php b/Neos.Flow/Classes/Security/RequestPattern/Uri.php index 9127691d1b..0121585cf1 100644 --- a/Neos.Flow/Classes/Security/RequestPattern/Uri.php +++ b/Neos.Flow/Classes/Security/RequestPattern/Uri.php @@ -21,14 +21,14 @@ class Uri implements RequestPatternInterface { /** - * @var array + * @var array */ protected $options; /** * Expects options in the form array('uriPattern' => '') * - * @param array $options + * @param array $options */ public function __construct(array $options) { diff --git a/Neos.Flow/Classes/Security/SessionDataContainer.php b/Neos.Flow/Classes/Security/SessionDataContainer.php index d35fb8e870..5741c6e519 100644 --- a/Neos.Flow/Classes/Security/SessionDataContainer.php +++ b/Neos.Flow/Classes/Security/SessionDataContainer.php @@ -4,6 +4,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Security\Authentication\Token\SessionlessTokenInterface; +use Neos\Flow\Security\Authentication\TokenInterface; /** * @Flow\Scope("session") @@ -14,14 +15,14 @@ class SessionDataContainer /** * The current list of security tokens. * - * @var array + * @var array */ protected $securityTokens = []; /** * The current list of CSRF tokens * - * @var array + * @var array */ protected $csrfProtectionTokens = []; @@ -35,7 +36,7 @@ class SessionDataContainer /** * Get the current list of security tokens. * - * @return array + * @return array */ public function getSecurityTokens(): array { @@ -45,9 +46,9 @@ public function getSecurityTokens(): array /** * Set the current list of security tokens with their data. * - * @param array $securityTokens + * @param array $securityTokens */ - public function setSecurityTokens(array $securityTokens) + public function setSecurityTokens(array $securityTokens): void { foreach ($securityTokens as $token) { if ($token instanceof SessionlessTokenInterface) { @@ -60,7 +61,7 @@ public function setSecurityTokens(array $securityTokens) /** * Get the current list of active CSRF tokens. * - * @return array + * @return array */ public function getCsrfProtectionTokens(): array { @@ -70,9 +71,9 @@ public function getCsrfProtectionTokens(): array /** * set the list of currently active CSRF tokens. * - * @param array $csrfProtectionTokens + * @param array $csrfProtectionTokens */ - public function setCsrfProtectionTokens(array $csrfProtectionTokens) + public function setCsrfProtectionTokens(array $csrfProtectionTokens): void { $this->csrfProtectionTokens = $csrfProtectionTokens; } diff --git a/Neos.Flow/Classes/Session/Aspect/LazyLoadingAspect.php b/Neos.Flow/Classes/Session/Aspect/LazyLoadingAspect.php index 6254dd84a7..996819e604 100644 --- a/Neos.Flow/Classes/Session/Aspect/LazyLoadingAspect.php +++ b/Neos.Flow/Classes/Session/Aspect/LazyLoadingAspect.php @@ -29,6 +29,9 @@ class LazyLoadingAspect #[Flow\Inject] protected ?LoggerInterface $logger = null; + /** + * @var array + */ protected array $sessionOriginalInstances = []; public function __construct( diff --git a/Neos.Flow/Classes/Session/Aspect/SessionObjectMethodsPointcutFilter.php b/Neos.Flow/Classes/Session/Aspect/SessionObjectMethodsPointcutFilter.php index 1f89a3be0b..631660cb1e 100644 --- a/Neos.Flow/Classes/Session/Aspect/SessionObjectMethodsPointcutFilter.php +++ b/Neos.Flow/Classes/Session/Aspect/SessionObjectMethodsPointcutFilter.php @@ -22,12 +22,20 @@ /** * Pointcut filter matching proxyable methods in objects of scope session + * + * @template ObjectRecordInstance of object */ #[Flow\Scope("singleton")] class SessionObjectMethodsPointcutFilter implements PointcutFilterInterface { + /** + * @phpstan-var CompileTimeObjectManager $objectManager + */ protected CompileTimeObjectManager $objectManager; + /** + * @phpstan-param CompileTimeObjectManager $objectManager + */ public function injectObjectManager(CompileTimeObjectManager $objectManager): void { $this->objectManager = $objectManager; @@ -37,7 +45,7 @@ public function injectObjectManager(CompileTimeObjectManager $objectManager): vo * Checks if the specified class and method matches against the filter * * @param string $className Name of the class to check against - * @param string $methodName Name of the method to check against + * @param ?string $methodName Name of the method to check against * @param string $methodDeclaringClassName Name of the class the method was originally declared in * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection. * @return bool true if the class / method match, otherwise false @@ -79,7 +87,7 @@ public function hasRuntimeEvaluationsDefinition(): bool /** * Returns runtime evaluations for a previously matched pointcut * - * @return array Runtime evaluations + * @return array Runtime evaluations */ public function getRuntimeEvaluationsDefinition(): array { diff --git a/Neos.Flow/Classes/Session/CookieEnabledInterface.php b/Neos.Flow/Classes/Session/CookieEnabledInterface.php index 522f07079c..c722999dad 100644 --- a/Neos.Flow/Classes/Session/CookieEnabledInterface.php +++ b/Neos.Flow/Classes/Session/CookieEnabledInterface.php @@ -18,5 +18,8 @@ interface CookieEnabledInterface extends SessionInterface { public function getSessionCookie(): Cookie; + /** + * @param array $tags + */ public static function createFromCookieAndSessionInformation(Cookie $sessionCookie, string $storageIdentifier, int $lastActivityTimestamp, array $tags = []): SessionInterface|CookieEnabledInterface; } diff --git a/Neos.Flow/Classes/Session/Data/SessionKeyValueStore.php b/Neos.Flow/Classes/Session/Data/SessionKeyValueStore.php index 18793fcaf5..a4ec948c1e 100644 --- a/Neos.Flow/Classes/Session/Data/SessionKeyValueStore.php +++ b/Neos.Flow/Classes/Session/Data/SessionKeyValueStore.php @@ -72,6 +72,9 @@ public function store(StorageIdentifier $storageIdentifier, string $key, mixed $ { $entryIdentifier = $this->createEntryIdentifier($storageIdentifier, $key); $serializedValue = ($this->useIgBinary === true) ? igbinary_serialize($value) : serialize($value); + if (is_null($serializedValue)) { + throw new \Exception('Failed to serialize value', 1743874462); + } $valueHash = md5($serializedValue); $debounceHash = $this->writeDebounceHashes[$storageIdentifier->value][$key] ?? null; if ($debounceHash !== null && $debounceHash === $valueHash) { @@ -90,7 +93,7 @@ public function remove(StorageIdentifier $storageIdentifier): int return $this->cache->flushByTag($storageIdentifier->value); } - private function createEntryIdentifier(StorageIdentifier $storageIdentifier, $key): string + private function createEntryIdentifier(StorageIdentifier $storageIdentifier, string $key): string { return $storageIdentifier->value . md5($key); } diff --git a/Neos.Flow/Classes/Session/Data/SessionMetaDataStore.php b/Neos.Flow/Classes/Session/Data/SessionMetaDataStore.php index a61eec9bff..81a21f9905 100644 --- a/Neos.Flow/Classes/Session/Data/SessionMetaDataStore.php +++ b/Neos.Flow/Classes/Session/Data/SessionMetaDataStore.php @@ -82,6 +82,15 @@ public function retrieve(SessionIdentifier $sessionIdentifier): ?SessionMetaData } if (is_array($metaDataFromCache)) { + if (!is_string($metaDataFromCache['storageIdentifier'] ?? null)) { + throw new \Exception('Missing storageIdentifier', 1743874214); + } + if (!is_int($metaDataFromCache['lastActivityTimestamp'])) { + throw new \Exception('Missing lastActivityTimestamp', 1743874236); + } + if (!is_array($metaDataFromCache['tags'])) { + throw new \Exception('Missing tags', 1743874272); + } $metaDataFromCache = SessionMetaData::createFromSessionIdentifierStringAndOldArrayCacheFormat($sessionIdentifier->value, $metaDataFromCache); $this->writeDebounceCache[$metaDataFromCache->sessionIdentifier->value] = $metaDataFromCache; return $metaDataFromCache; @@ -104,6 +113,15 @@ public function retrieveByTag(string $tag): \Generator $this->writeDebounceCache[$sessionIdentifier] = $sessionMetaData; yield $sessionIdentifier => $sessionMetaData; } elseif (is_array($sessionMetaData)) { + if (!is_string($sessionMetaData['storageIdentifier'] ?? null)) { + throw new \Exception('Missing storageIdentifier', 1743874214); + } + if (!is_int($sessionMetaData['lastActivityTimestamp'])) { + throw new \Exception('Missing lastActivityTimestamp', 1743874236); + } + if (!is_array($sessionMetaData['tags'])) { + throw new \Exception('Missing tags', 1743874272); + } $sessionMetaData = SessionMetaData::createFromSessionIdentifierStringAndOldArrayCacheFormat($sessionIdentifier, $sessionMetaData); $this->writeDebounceCache[$sessionIdentifier] = $sessionMetaData; yield $sessionIdentifier => $sessionMetaData; @@ -125,7 +143,19 @@ public function retrieveAll(): \Generator $this->writeDebounceCache[$sessionIdentifier] = $sessionMetaData; yield $sessionIdentifier => $sessionMetaData; } elseif (is_array($sessionMetaData)) { - $sessionMetaData = SessionMetaData::createFromSessionIdentifierStringAndOldArrayCacheFormat($sessionIdentifier, $sessionMetaData); + if (!is_string($sessionMetaData['storageIdentifier'] ?? null)) { + throw new \Exception('Missing storageIdentifier', 1743874214); + } + if (!is_int($sessionMetaData['lastActivityTimestamp'])) { + throw new \Exception('Missing lastActivityTimestamp', 1743874236); + } + if (!is_array($sessionMetaData['tags'])) { + throw new \Exception('Missing tags', 1743874272); + } + $sessionMetaData = SessionMetaData::createFromSessionIdentifierStringAndOldArrayCacheFormat( + $sessionIdentifier, + $sessionMetaData + ); $this->writeDebounceCache[$sessionIdentifier] = $sessionMetaData; yield $sessionIdentifier => $sessionMetaData; } diff --git a/Neos.Flow/Classes/Session/Session.php b/Neos.Flow/Classes/Session/Session.php index 1d67a029b9..c7296bc8ab 100644 --- a/Neos.Flow/Classes/Session/Session.php +++ b/Neos.Flow/Classes/Session/Session.php @@ -71,6 +71,9 @@ class Session implements CookieEnabledInterface protected ?string $sessionCookieSameSite = null; protected ?Cookie $sessionCookie = null; protected int $inactivityTimeout; + /** + * @var array + */ protected array $tags = []; protected int $now = 0; protected ?SessionMetaData $sessionMetaData = null; @@ -91,7 +94,10 @@ public static function create(): self return new static(); } - public static function createRemote(string $sessionIdentifier, string $storageIdentifier, ?int $lastActivityTimestamp = null, array $tags = []): self + /** + * @param array $tags + */ + public static function createRemote(string $sessionIdentifier, string $storageIdentifier, int $lastActivityTimestamp, array $tags = []): self { $session = new static(); $session->sessionMetaData = new SessionMetaData( @@ -114,6 +120,9 @@ public static function createRemoteFromSessionMetaData(SessionMetaData $sessionM return $session; } + /** + * @param array $tags + */ public static function createFromCookieAndSessionInformation(Cookie $sessionCookie, string $storageIdentifier, int $lastActivityTimestamp, array $tags = []): SessionInterface|CookieEnabledInterface { $session = new static(); @@ -127,6 +136,9 @@ public static function createFromCookieAndSessionInformation(Cookie $sessionCook return $session; } + /** + * @param array $settings + */ public function injectSettings(array $settings): void { $this->sessionCookieName = $settings['session']['name']; @@ -141,6 +153,9 @@ public function injectSettings(array $settings): void public function getSessionCookie(): Cookie { + if (!$this->sessionCookie) { + throw new \Exception('Missing session cookie'); + } return $this->sessionCookie; } @@ -221,6 +236,9 @@ public function canBeResumed(): bool public function resume(): ?int { if ($this->started === false && $this->canBeResumed()) { + if (!$this->sessionMetaData) { + throw new \Exception('Cannot resume a session without metadata'); + } $this->started = true; $sessionObjects = $this->sessionKeyValueStore->retrieve($this->sessionMetaData->storageIdentifier, self::FLOW_OBJECT_STORAGE_KEY); @@ -256,7 +274,7 @@ public function resume(): ?int */ public function getId(): string { - if ($this->started !== true) { + if ($this->started !== true || !$this->sessionMetaData) { throw new Exception\SessionNotStartedException('Tried to retrieve the session identifier, but the session has not been started yet.)', 1351171517); } return $this->sessionMetaData->sessionIdentifier->value; @@ -276,6 +294,12 @@ public function renewId(): string if ($this->started !== true) { throw new Exception\SessionNotStartedException('Tried to renew the session identifier, but the session has not been started yet.', 1351182429); } + if (!$this->sessionMetaData) { + throw new \Exception('Missing session metadata'); + } + if (!$this->sessionCookie) { + throw new \Exception('Missing session cookie'); + } if ($this->remote === true) { throw new Exception\OperationNotSupportedException(sprintf('Tried to renew the session identifier on a remote session (%s).', $this->sessionMetaData->sessionIdentifier->value), 1354034230); } @@ -284,6 +308,12 @@ public function renewId(): string $this->sessionMetaData = $this->sessionMetaData->withNewSessionIdentifier(); $this->writeSessionMetaDataCacheEntry(); + if (!$this->sessionMetaData) { + throw new \Exception('Missing session metadata'); + } + if (!$this->sessionCookie) { + throw new \Exception('Missing session cookie'); + } $this->sessionCookie->setValue($this->sessionMetaData->sessionIdentifier->value); return $this->sessionMetaData->sessionIdentifier->value; } @@ -297,7 +327,7 @@ public function renewId(): string */ public function getData(string $key): mixed { - if ($this->started !== true) { + if ($this->started !== true || !$this->sessionMetaData) { throw new Exception\SessionNotStartedException('Tried to get session data, but the session has not been started yet.', 1351162255); } return $this->sessionKeyValueStore->retrieve($this->sessionMetaData->storageIdentifier, $key); @@ -312,7 +342,7 @@ public function getData(string $key): mixed */ public function hasKey(string $key): bool { - if ($this->started !== true) { + if ($this->started !== true || !$this->sessionMetaData) { throw new Exception\SessionNotStartedException('Tried to check a session data entry, but the session has not been started yet.', 1352488661); } return $this->sessionKeyValueStore->has($this->sessionMetaData->storageIdentifier, $key); @@ -332,7 +362,7 @@ public function hasKey(string $key): bool */ public function putData(string $key, mixed $data): void { - if ($this->started !== true) { + if ($this->started !== true || !$this->sessionMetaData) { throw new Exception\SessionNotStartedException('Tried to create a session data entry, but the session has not been started yet.', 1351162259); } if (is_resource($data)) { @@ -354,7 +384,7 @@ public function putData(string $key, mixed $data): void */ public function getLastActivityTimestamp(): int { - if ($this->started !== true) { + if ($this->started !== true || !$this->sessionMetaData) { throw new Exception\SessionNotStartedException('Tried to retrieve the last activity timestamp of a session which has not been started yet.', 1354290378); } return $this->sessionMetaData->lastActivityTimestamp; @@ -378,9 +408,9 @@ public function addTag(string $tag): void throw new Exception\SessionNotStartedException('Tried to tag a session which has not been started yet.', 1355143533); } if (!$this->sessionMetaDataStore->isValidSessionTag($tag)) { - throw new \InvalidArgumentException(sprintf('The tag used for tagging session %s contained invalid characters. Make sure it matches this regular expression: "%s"', $this->sessionMetaData->sessionIdentifier->value, FrontendInterface::PATTERN_TAG)); + throw new \InvalidArgumentException(sprintf('The tag used for tagging session %s contained invalid characters. Make sure it matches this regular expression: "%s"', $this->sessionMetaData?->sessionIdentifier->value ?: '', FrontendInterface::PATTERN_TAG)); } - $this->sessionMetaData = $this->sessionMetaData->withAddedTag($tag); + $this->sessionMetaData = $this->sessionMetaData?->withAddedTag($tag); } /** @@ -396,14 +426,14 @@ public function removeTag(string $tag): void if ($this->started !== true) { throw new Exception\SessionNotStartedException('Tried to tag a session which has not been started yet.', 1355150140); } - $this->sessionMetaData = $this->sessionMetaData->withRemovedTag($tag); + $this->sessionMetaData = $this->sessionMetaData?->withRemovedTag($tag); } /** * Returns the tags this session has been tagged with. * - * @return array The tags or an empty array if there aren't any + * @return array The tags or an empty array if there aren't any * @throws Exception\SessionNotStartedException * @api */ @@ -412,7 +442,7 @@ public function getTags(): array if ($this->started !== true) { throw new Exception\SessionNotStartedException('Tried to retrieve tags from a session which has not been started yet.', 1355141501); } - return $this->sessionMetaData->tags; + return $this->sessionMetaData?->tags ?: []; } /** @@ -428,7 +458,7 @@ public function touch(): void // Only makes sense for remote sessions because the currently active session // will be updated on shutdown anyway: - if ($this->remote === true) { + if ($this->remote === true && $this->sessionMetaData) { $this->sessionMetaData = $this->sessionMetaData->withLastActivityTimestamp($this->now); $this->writeSessionMetaDataCacheEntry(); } @@ -461,12 +491,14 @@ public function destroy(?string $reason = null): void } if ($this->remote !== true) { - $this->sessionCookie->expire(); + $this->sessionCookie?->expire(); } - $this->sessionMetaDataStore->remove($this->sessionMetaData); - $this->sessionKeyValueStore->remove($this->sessionMetaData->storageIdentifier); - $this->sessionMetaData = null; + if ($this->sessionMetaData) { + $this->sessionMetaDataStore->remove($this->sessionMetaData); + $this->sessionKeyValueStore->remove($this->sessionMetaData->storageIdentifier); + $this->sessionMetaData = null; + } $this->started = false; } @@ -482,8 +514,12 @@ public function destroy(?string $reason = null): void public function shutdownObject(): void { if ($this->started === true && $this->remote === false) { - if ($this->sessionMetaDataStore->has($this->sessionMetaData->sessionIdentifier)) { - $this->sessionKeyValueStore->store($this->sessionMetaData->storageIdentifier, self::FLOW_OBJECT_STORAGE_KEY, $this->objectManager->getSessionInstances() ?? []); + if ($this->sessionMetaData && $this->sessionMetaDataStore->has($this->sessionMetaData->sessionIdentifier)) { + $this->sessionKeyValueStore->store( + $this->sessionMetaData->storageIdentifier, + self::FLOW_OBJECT_STORAGE_KEY, + $this->objectManager->getSessionInstances() + ); $this->writeSessionMetaDataCacheEntry(); } $this->started = false; @@ -497,11 +533,11 @@ public function shutdownObject(): void */ protected function autoExpire(): bool { - $lastActivitySecondsAgo = $this->now - $this->sessionMetaData->lastActivityTimestamp; + $lastActivitySecondsAgo = $this->now - ($this->sessionMetaData?->lastActivityTimestamp ?: 0); $expired = false; if ($this->inactivityTimeout !== 0 && $lastActivitySecondsAgo > $this->inactivityTimeout) { $this->started = true; - $this->destroy(sprintf('Session %s was inactive for %s seconds, more than the configured timeout of %s seconds.', $this->sessionMetaData->sessionIdentifier->value, $lastActivitySecondsAgo, $this->inactivityTimeout)); + $this->destroy(sprintf('Session %s was inactive for %s seconds, more than the configured timeout of %s seconds.', $this->sessionMetaData?->sessionIdentifier->value ?: '', $lastActivitySecondsAgo, $this->inactivityTimeout)); $expired = true; } return $expired; @@ -521,6 +557,8 @@ protected function autoExpire(): bool */ protected function writeSessionMetaDataCacheEntry(): void { - $this->sessionMetaDataStore->store($this->sessionMetaData); + if ($this->sessionMetaData) { + $this->sessionMetaDataStore->store($this->sessionMetaData); + } } } diff --git a/Neos.Flow/Classes/Session/SessionInterface.php b/Neos.Flow/Classes/Session/SessionInterface.php index 791f21debf..b9c8b51485 100644 --- a/Neos.Flow/Classes/Session/SessionInterface.php +++ b/Neos.Flow/Classes/Session/SessionInterface.php @@ -107,7 +107,7 @@ public function removeTag(string $tag): void; /** * Returns the tags this session has been tagged with. * - * @return array The tags or an empty array if there aren't any + * @return array The tags or an empty array if there aren't any * @throws Exception\SessionNotStartedException * @api */ diff --git a/Neos.Flow/Classes/Session/SessionManager.php b/Neos.Flow/Classes/Session/SessionManager.php index cdf8d89a90..c6629baccd 100644 --- a/Neos.Flow/Classes/Session/SessionManager.php +++ b/Neos.Flow/Classes/Session/SessionManager.php @@ -27,6 +27,10 @@ class SessionManager implements SessionManagerInterface { protected ?SessionInterface $currentSession = null; + + /** + * @var array + */ protected array $remoteSessions = []; public function __construct( @@ -139,7 +143,7 @@ public function getActiveSessions(): array * Returns all sessions which are tagged by the specified tag. * * @param string $tag A valid Cache Frontend tag - * @return array A collection of Session objects or an empty array if tag did not match + * @return array A collection of Session objects or an empty array if tag did not match * @throws NotSupportedByBackendException * @api */ @@ -195,10 +199,8 @@ public function collectGarbage(): ?int foreach ($this->sessionMetaDataStore->retrieveAll() as $sessionMetadata) { $lastActivitySecondsAgo = $now - $sessionMetadata->lastActivityTimestamp; if ($lastActivitySecondsAgo > $this->inactivityTimeout) { - if ($sessionMetadata->lastActivityTimestamp !== null) { - $this->sessionKeyValueStore->remove($sessionMetadata->storageIdentifier); - $sessionRemovalCount++; - } + $this->sessionKeyValueStore->remove($sessionMetadata->storageIdentifier); + $sessionRemovalCount++; $this->sessionMetaDataStore->remove($sessionMetadata); } if ($sessionRemovalCount >= $this->garbageCollectionMaximumPerRun) { @@ -223,7 +225,7 @@ public function collectGarbage(): ?int public function shutdownObject(): void { if (str_contains((string)$this->garbageCollectionProbability, '.')) { - $decimals = strlen(strrchr((string)$this->garbageCollectionProbability, '.')) - 1; + $decimals = strlen(strrchr((string)$this->garbageCollectionProbability, '.') ?: '') - 1; $factor = $decimals * 10; } else { $factor = 1; diff --git a/Neos.Flow/Classes/Session/SessionManagerInterface.php b/Neos.Flow/Classes/Session/SessionManagerInterface.php index de71da87fd..1455968e47 100644 --- a/Neos.Flow/Classes/Session/SessionManagerInterface.php +++ b/Neos.Flow/Classes/Session/SessionManagerInterface.php @@ -51,7 +51,7 @@ public function getActiveSessions(): array; * Returns all sessions which are tagged by the specified tag. * * @param string $tag A valid Cache Frontend tag - * @return array A collection of Session objects or an empty array if tag did not match + * @return array A collection of Session objects or an empty array if tag did not match * @api */ public function getSessionsByTag(string $tag): array; diff --git a/Neos.Flow/Classes/Session/TransientSession.php b/Neos.Flow/Classes/Session/TransientSession.php index 0e15bb8231..ad9fbdcb6f 100644 --- a/Neos.Flow/Classes/Session/TransientSession.php +++ b/Neos.Flow/Classes/Session/TransientSession.php @@ -26,8 +26,14 @@ class TransientSession implements SessionInterface { protected string $sessionId; protected bool $started = false; + /** + * @var array + */ protected array $data = []; protected ?int $lastActivityTimestamp = null; + /** + * @var array + */ protected array $tags; public function isStarted(): bool @@ -165,6 +171,7 @@ public function getLastActivityTimestamp(): int if ($this->lastActivityTimestamp === null) { $this->touch(); } + /** @phpstan-ignore return.type (touch() initializes the timestamp) */ return $this->lastActivityTimestamp; } @@ -185,7 +192,6 @@ public function touch(): void * @param string $tag The tag – must match be a valid cache frontend tag * @return void * @throws Exception\SessionNotStartedException - * @throws \InvalidArgumentException * @api */ public function addTag(string $tag): void @@ -217,7 +223,7 @@ public function removeTag(string $tag): void /** * Returns the tags this session has been tagged with. * - * @return array The tags or an empty array if there aren't any + * @return array The tags or an empty array if there aren't any * @throws Exception\SessionNotStartedException * @api */ diff --git a/Neos.Flow/Classes/SignalSlot/Dispatcher.php b/Neos.Flow/Classes/SignalSlot/Dispatcher.php index b802e14517..947690d4d3 100644 --- a/Neos.Flow/Classes/SignalSlot/Dispatcher.php +++ b/Neos.Flow/Classes/SignalSlot/Dispatcher.php @@ -24,18 +24,21 @@ */ class Dispatcher { - /** - * @var ObjectManagerInterface - */ - protected $objectManager; + protected ?ObjectManagerInterface $objectManager = null; /** * Information about all slots connected a certain signal. * Indexed by [$signalClassName][$signalMethodName] and then numeric with an * array of information about the slot - * @var array + * @var array>> */ - protected $slots = []; + protected array $slots = []; /** * Injects the object manager @@ -56,7 +59,7 @@ public function injectObjectManager(ObjectManagerInterface $objectManager): void * When $passSignalInformation is true, the slot will be passed a string (EmitterClassName::signalName) as the last * parameter. * - * @param string $signalClassName Name of the class containing the signal + * @param class-string $signalClassName Name of the class containing the signal * @param string $signalName Name of the signal * @param mixed $slotClassNameOrObject Name of the class containing the slot or the instantiated class or a Closure object * @param string $slotMethodName Name of the method to be used as a slot. If $slotClassNameOrObject is a Closure object, this parameter is ignored @@ -78,7 +81,7 @@ public function connect(string $signalClassName, string $signalName, $slotClassN * * The slot will be passed a an instance of SignalInformation as the sole parameter. * - * @param string $signalClassName Name of the class containing the signal + * @param class-string $signalClassName Name of the class containing the signal * @param string $signalName Name of the signal * @param mixed $slotClassNameOrObject Name of the class containing the slot or the instantiated class or a Closure object * @param string $slotMethodName Name of the method to be used as a slot. If $slotClassNameOrObject is a Closure object, this parameter is ignored @@ -94,7 +97,7 @@ public function wire(string $signalClassName, string $signalName, $slotClassName } /** - * @param string $signalClassName + * @param class-string $signalClassName * @param string $signalName * @param mixed $slotClassNameOrObject * @param string $slotMethodName @@ -137,7 +140,7 @@ private function connectSignalToSlot(string $signalClassName, string $signalName * * @param string $signalClassName Name of the class containing the signal * @param string $signalName Name of the signal - * @param array $signalArguments arguments passed to the signal method + * @param array $signalArguments arguments passed to the signal method * @return void * @throws Exception\InvalidSlotException if the slot is not valid * @api @@ -153,37 +156,48 @@ public function dispatch(string $signalClassName, string $signalName, array $sig if (isset($slotInformation['object'])) { $object = $slotInformation['object']; } elseif (strpos($slotInformation['method'], '::') === 0) { - if (!isset($this->objectManager)) { + if ($this->objectManager === null) { if (is_callable($slotInformation['class'] . $slotInformation['method'])) { $object = $slotInformation['class']; } else { throw new Exception\InvalidSlotException(sprintf('Cannot dispatch %s::%s to class %s. The object manager is not yet available in the Signal Slot Dispatcher and therefore it cannot dispatch classes.', $signalClassName, $signalName, $slotInformation['class']), 1298113624); } } else { + if ($slotInformation['class'] === null) { + throw new Exception\InvalidSlotException('Slot class is undefined.', 1743958214); + } $object = $this->objectManager->getClassNameByObjectName($slotInformation['class']); } $slotInformation['method'] = substr($slotInformation['method'], 2); } else { - if (!isset($this->objectManager)) { + if ($this->objectManager === null) { throw new Exception\InvalidSlotException(sprintf('Cannot dispatch %s::%s to class %s. The object manager is not yet available in the Signal Slot Dispatcher and therefore it cannot dispatch classes.', $signalClassName, $signalName, $slotInformation['class']), 1298113624); } + if ($slotInformation['class'] === null) { + throw new Exception\InvalidSlotException('Slot class is undefined.', 1743958214); + } if (!$this->objectManager->isRegistered($slotInformation['class'])) { throw new Exception\InvalidSlotException('The given class "' . $slotInformation['class'] . '" is not a registered object.', 1245673367); } $object = $this->objectManager->get($slotInformation['class']); } - if (!method_exists($object, $slotInformation['method'])) { + if (is_object($object) && !method_exists($object, $slotInformation['method'])) { throw new Exception\InvalidSlotException('The slot method ' . get_class($object) . '->' . $slotInformation['method'] . '() does not exist.', 1245673368); } + $callable = [$object, $slotInformation['method']]; + if (!is_callable($callable)) { + throw new \Exception('Invalid slot method ' . $slotInformation['method'], 1743955562); + } + if ($slotInformation['useSignalInformationObject'] === true) { - call_user_func([$object, $slotInformation['method']], new SignalInformation($signalClassName, $signalName, $finalSignalArguments)); + call_user_func($callable, new SignalInformation($signalClassName, $signalName, $finalSignalArguments)); } else { if ($slotInformation['passSignalInformation'] === true) { $finalSignalArguments[] = $signalClassName . '::' . $signalName; } // Need to use call_user_func_array here, because $object may be the class name when the slot is a static method - call_user_func_array([$object, $slotInformation['method']], $finalSignalArguments); + call_user_func_array($callable, $finalSignalArguments); } } } @@ -191,9 +205,15 @@ public function dispatch(string $signalClassName, string $signalName, array $sig /** * Returns all slots which are connected with the given signal * - * @param string $signalClassName Name of the class containing the signal + * @param class-string $signalClassName Name of the class containing the signal * @param string $signalName Name of the signal - * @return array An array of arrays with slot information + * @return array An array of arrays with slot information * @api */ public function getSlots(string $signalClassName, string $signalName): array @@ -204,7 +224,13 @@ public function getSlots(string $signalClassName, string $signalName): array /** * Returns all signals with its slots * - * @return array An array of arrays with slot information + * @return array>> An array of arrays with slot information * @api */ public function getSignals(): array diff --git a/Neos.Flow/Classes/SignalSlot/SignalInformation.php b/Neos.Flow/Classes/SignalSlot/SignalInformation.php index 650b133a76..3ad58347bc 100644 --- a/Neos.Flow/Classes/SignalSlot/SignalInformation.php +++ b/Neos.Flow/Classes/SignalSlot/SignalInformation.php @@ -35,10 +35,13 @@ final class SignalInformation protected $signalName; /** - * @var array + * @var array */ protected $signalArguments; + /** + * @param array $signalArguments + */ public function __construct(string $signalClassName, string $signalName, array $signalArguments) { $this->signalClassName = $signalClassName; @@ -56,6 +59,9 @@ public function getSignalName(): string return $this->signalName; } + /** + * @return array + */ public function getSignalArguments(): array { return $this->signalArguments; diff --git a/Neos.Flow/Classes/Utility/Algorithms.php b/Neos.Flow/Classes/Utility/Algorithms.php index 9991b4cfe4..fb46608466 100644 --- a/Neos.Flow/Classes/Utility/Algorithms.php +++ b/Neos.Flow/Classes/Utility/Algorithms.php @@ -36,7 +36,7 @@ class Algorithms */ public static function generateUUID(): string { - if (is_callable('uuid_create')) { + if (function_exists('uuid_create')) { return strtolower(uuid_create(UUID_TYPE_RANDOM)); } diff --git a/Neos.Flow/Classes/Utility/Environment.php b/Neos.Flow/Classes/Utility/Environment.php index 44787d3f87..f5e20e39eb 100644 --- a/Neos.Flow/Classes/Utility/Environment.php +++ b/Neos.Flow/Classes/Utility/Environment.php @@ -16,7 +16,6 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Core\ApplicationContext; use Neos\Flow\Core\Bootstrap; -use Neos\Flow\Error\Exception as ErrorException; use Neos\Flow\Utility\Exception as UtilityException; use Neos\Flow\Mvc\ActionRequest; use Neos\Utility\Exception\FilesException; @@ -140,18 +139,15 @@ public static function composeTemporaryDirectoryName(string $temporaryDirectoryB * * @param string $temporaryDirectoryBase Full path to the base for the temporary directory * @return string The full path to the temporary directory - * @throws UtilityException if the temporary directory could not be created or is not writable + * @throws FilesException if the temporary directory could not be created + * @throws UtilityException if the temporary directory is not writable */ protected function createTemporaryDirectory(string $temporaryDirectoryBase): string { $temporaryDirectory = self::composeTemporaryDirectoryName($temporaryDirectoryBase, $this->context); if (!is_dir($temporaryDirectory) && !is_link($temporaryDirectory)) { - try { - Files::createDirectoryRecursively($temporaryDirectory); - } catch (ErrorException $exception) { - throw new UtilityException('The temporary directory "' . $temporaryDirectory . '" could not be created. Please make sure permissions are correct for this path or define another temporary directory in your Settings.yaml with the path "Neos.Flow.utility.environment.temporaryDirectoryBase".', 1335382361); - } + Files::createDirectoryRecursively($temporaryDirectory); } if (!is_writable($temporaryDirectory)) { diff --git a/Neos.Flow/Classes/Validation/Validator/AbstractCompositeValidator.php b/Neos.Flow/Classes/Validation/Validator/AbstractCompositeValidator.php index 4803782fa4..dba8d72acb 100644 --- a/Neos.Flow/Classes/Validation/Validator/AbstractCompositeValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/AbstractCompositeValidator.php @@ -24,29 +24,29 @@ abstract class AbstractCompositeValidator implements ObjectValidatorInterface, \ /** * This contains the supported options, their default values and descriptions. * - * @var array + * @var array */ protected $supportedOptions = []; /** - * @var array + * @var array */ protected $options = []; /** - * @var \SplObjectStorage + * @var \SplObjectStorage */ protected $validators; /** - * @var \SplObjectStorage + * @var \SplObjectStorage */ protected $validatedInstancesContainer; /** * Constructs the composite validator and sets validation options * - * @param array $options Options for the validator + * @param array $options Options for the validator * @api * @throws InvalidValidationOptionsException */ @@ -84,7 +84,7 @@ function ($value) { /** * Allows to set a container to keep track of validated instances. * - * @param \SplObjectStorage $validatedInstancesContainer A container to keep track of validated instances + * @param \SplObjectStorage $validatedInstancesContainer A container to keep track of validated instances * @return void * @api */ @@ -102,7 +102,7 @@ public function setValidatedInstancesContainer(\SplObjectStorage $validatedInsta */ public function addValidator(ValidatorInterface $validator) { - if ($validator instanceof ObjectValidatorInterface && isset($this->validatedInstancesContainer)) { + if ($validator instanceof ObjectValidatorInterface) { $validator->setValidatedInstancesContainer($this->validatedInstancesContainer); } $this->validators->attach($validator); @@ -115,7 +115,7 @@ public function addValidator(ValidatorInterface $validator) * @throws NoSuchValidatorException * @api */ - public function removeValidator(ValidatorInterface $validator) + public function removeValidator(ValidatorInterface $validator): void { if (!$this->validators->contains($validator)) { throw new NoSuchValidatorException('Cannot remove validator because its not in the conjunction.', 1207020177); @@ -137,7 +137,7 @@ public function count(): int /** * Returns the child validators of this Composite Validator * - * @return \SplObjectStorage + * @return \SplObjectStorage */ public function getValidators() { @@ -147,7 +147,7 @@ public function getValidators() /** * Returns the options for this validator * - * @return array + * @return array */ public function getOptions() { diff --git a/Neos.Flow/Classes/Validation/Validator/AbstractValidator.php b/Neos.Flow/Classes/Validation/Validator/AbstractValidator.php index 7a11ff35e9..9f91868935 100644 --- a/Neos.Flow/Classes/Validation/Validator/AbstractValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/AbstractValidator.php @@ -41,17 +41,17 @@ abstract class AbstractValidator implements ValidatorInterface * 2 => type * 3 => required (boolean, optional) * - * @var array + * @var array */ protected $supportedOptions = []; /** - * @var array + * @var array */ protected $options = []; /** - * @var ErrorResult + * @var ErrorResult|null */ private $result; @@ -63,7 +63,7 @@ abstract class AbstractValidator implements ValidatorInterface /** * Constructs the validator and sets validation options * - * @param array $options Options for the validator + * @param array $options Options for the validator * @throws InvalidValidationOptionsException if unsupported options are found * @api */ @@ -117,7 +117,7 @@ protected function pushResult() /** * Pop and return the current Result from the stack and make $this->result point to the last Result again. * @since Flow 4.3 - * @return ErrorResult + * @return ErrorResult|null */ protected function popResult() { @@ -143,7 +143,7 @@ protected function getResult() * the Error Messages object which occurred. * * @param mixed $value The value that should be validated - * @return ErrorResult + * @return ErrorResult|null * @throws InvalidValidationOptionsException * @api */ @@ -171,19 +171,22 @@ abstract protected function isValid($value); * * @param string $message The error message * @param integer $code The error code (a unix timestamp) - * @param array $arguments Arguments to be replaced in message + * @param array $arguments Arguments to be replaced in message * @return void * @api */ protected function addError($message, $code, array $arguments = []) { + if ($this->result === null) { + $this->result = new ErrorResult(); + } $this->result->addError(new ValidationError($message, $code, $arguments)); } /** * Returns the options of this validator * - * @return array + * @return array */ public function getOptions() { diff --git a/Neos.Flow/Classes/Validation/Validator/BooleanValueValidator.php b/Neos.Flow/Classes/Validation/Validator/BooleanValueValidator.php index be9ac2833f..c0d773440c 100644 --- a/Neos.Flow/Classes/Validation/Validator/BooleanValueValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/BooleanValueValidator.php @@ -22,7 +22,7 @@ class BooleanValueValidator extends AbstractValidator { /** - * @var array + * @var array */ protected $supportedOptions = [ 'expectedValue' => [true, 'The expected boolean value', 'boolean'] diff --git a/Neos.Flow/Classes/Validation/Validator/CollectionValidator.php b/Neos.Flow/Classes/Validation/Validator/CollectionValidator.php index e02bf51efb..85376bc5ca 100644 --- a/Neos.Flow/Classes/Validation/Validator/CollectionValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/CollectionValidator.php @@ -22,7 +22,7 @@ class CollectionValidator extends GenericObjectValidator { /** - * @var array + * @var array */ protected $supportedOptions = [ 'elementValidator' => [null, 'The validator type to use for the collection elements', 'string'], @@ -52,7 +52,7 @@ protected function isValid($value) { if ($value instanceof \Doctrine\Common\Collections\AbstractLazyCollection && !$value->isInitialized()) { return; - } elseif ((is_object($value) && !TypeHandling::isCollectionType(get_class($value))) && !is_array($value)) { + } elseif (is_object($value) && !TypeHandling::isCollectionType(get_class($value))) { $this->addError('The given subject was not a collection.', 1317204797); return; } elseif (is_object($value) && $this->isValidatedAlready($value)) { @@ -75,7 +75,9 @@ protected function isValid($value) $collectionElementValidator->setValidatedInstancesContainer($this->validatedInstancesContainer); } - $this->getResult()->forProperty($index)->merge($collectionElementValidator->validate($collectionElement)); + if ($this->getResult() && $collectionElementValidator) { + $this->getResult()->forProperty($index)->merge($collectionElementValidator->validate($collectionElement)); + } } } } diff --git a/Neos.Flow/Classes/Validation/Validator/ConjunctionValidator.php b/Neos.Flow/Classes/Validation/Validator/ConjunctionValidator.php index 1dc9ff520b..c2bbd67562 100644 --- a/Neos.Flow/Classes/Validation/Validator/ConjunctionValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/ConjunctionValidator.php @@ -25,7 +25,7 @@ class ConjunctionValidator extends AbstractCompositeValidator * Every validator has to be valid, to make the whole conjunction valid. * * @param mixed $value The value that should be validated - * @return ErrorResult + * @return ErrorResult|null * @api */ public function validate($value) diff --git a/Neos.Flow/Classes/Validation/Validator/CountValidator.php b/Neos.Flow/Classes/Validation/Validator/CountValidator.php index 97a2b8cc7d..9e80afb73e 100644 --- a/Neos.Flow/Classes/Validation/Validator/CountValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/CountValidator.php @@ -20,7 +20,7 @@ class CountValidator extends AbstractValidator { /** - * @var array + * @var array */ protected $supportedOptions = [ 'minimum' => [0, 'The minimum count to accept', 'integer'], diff --git a/Neos.Flow/Classes/Validation/Validator/DateTimeRangeValidator.php b/Neos.Flow/Classes/Validation/Validator/DateTimeRangeValidator.php index cf5266d104..1e9a2fece4 100644 --- a/Neos.Flow/Classes/Validation/Validator/DateTimeRangeValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/DateTimeRangeValidator.php @@ -21,7 +21,7 @@ class DateTimeRangeValidator extends AbstractValidator { /** - * @var array + * @var array */ protected $supportedOptions = [ 'latestDate' => [null, 'The latest date to accept', 'string'], diff --git a/Neos.Flow/Classes/Validation/Validator/DateTimeValidator.php b/Neos.Flow/Classes/Validation/Validator/DateTimeValidator.php index db14ba9293..ebd10f2c2d 100644 --- a/Neos.Flow/Classes/Validation/Validator/DateTimeValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/DateTimeValidator.php @@ -23,7 +23,7 @@ class DateTimeValidator extends AbstractValidator { /** - * @var array + * @var array */ protected $supportedOptions = [ 'locale' => [null, 'The locale to use for date parsing', 'string|Locale'], diff --git a/Neos.Flow/Classes/Validation/Validator/DisjunctionValidator.php b/Neos.Flow/Classes/Validation/Validator/DisjunctionValidator.php index 605a2c69c5..6373ac2c47 100644 --- a/Neos.Flow/Classes/Validation/Validator/DisjunctionValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/DisjunctionValidator.php @@ -28,7 +28,7 @@ class DisjunctionValidator extends AbstractCompositeValidator * Errors are only returned if all validators failed. * * @param mixed $value The value that should be validated - * @return ErrorResult + * @return ErrorResult|null * @api */ public function validate($value) diff --git a/Neos.Flow/Classes/Validation/Validator/EmailAddressValidator.php b/Neos.Flow/Classes/Validation/Validator/EmailAddressValidator.php index bf9609d4cb..2d814e1bc8 100644 --- a/Neos.Flow/Classes/Validation/Validator/EmailAddressValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/EmailAddressValidator.php @@ -27,7 +27,7 @@ class EmailAddressValidator extends AbstractValidator { /** - * @var array + * @var array */ protected $supportedOptions = [ 'strict' => [false, 'Whether to fail validation on RFC warnings', 'bool'], diff --git a/Neos.Flow/Classes/Validation/Validator/FileExtensionValidator.php b/Neos.Flow/Classes/Validation/Validator/FileExtensionValidator.php index c3534f7ff4..fd72ac0bb5 100644 --- a/Neos.Flow/Classes/Validation/Validator/FileExtensionValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/FileExtensionValidator.php @@ -31,7 +31,7 @@ class FileExtensionValidator extends AbstractValidator * 2 => type * 3 => required (boolean, optional) * - * @var array + * @var array */ protected $supportedOptions = [ 'allowedExtensions' => [[], 'Array of allowed file extensions', 'array', true] @@ -58,9 +58,9 @@ protected function isValid($value) return; } - $fileExtension = pathinfo((string)$filename, PATHINFO_EXTENSION); + $fileExtension = pathinfo((string)$filename, PATHINFO_EXTENSION); - if ($fileExtension === null || $fileExtension === '') { + if ($fileExtension === '') { $this->addError('The file has no file extension.', 1677934932); return; } diff --git a/Neos.Flow/Classes/Validation/Validator/FileSizeValidator.php b/Neos.Flow/Classes/Validation/Validator/FileSizeValidator.php index 13283f7624..7507d821ad 100644 --- a/Neos.Flow/Classes/Validation/Validator/FileSizeValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/FileSizeValidator.php @@ -31,7 +31,7 @@ class FileSizeValidator extends AbstractValidator * 2 => type * 3 => required (boolean, optional) * - * @var array + * @var array */ protected $supportedOptions = [ 'minimum' => [null, 'Minimum allowed filesize in bytes', 'integer', false], diff --git a/Neos.Flow/Classes/Validation/Validator/GenericObjectValidator.php b/Neos.Flow/Classes/Validation/Validator/GenericObjectValidator.php index e211fc144b..bd38927e04 100644 --- a/Neos.Flow/Classes/Validation/Validator/GenericObjectValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/GenericObjectValidator.php @@ -23,26 +23,26 @@ class GenericObjectValidator extends AbstractValidator implements ObjectValidatorInterface { /** - * @var array + * @var array */ protected $supportedOptions = [ 'skipUnInitializedProxies' => [false, 'Whether proxies not yet initialized should be skipped during validation', 'boolean'] ]; /** - * @var array + * @var array> */ protected $propertyValidators = []; /** - * @var \SplObjectStorage + * @var \SplObjectStorage */ protected $validatedInstancesContainer; /** * Allows to set a container to keep track of validated instances. * - * @param \SplObjectStorage $validatedInstancesContainer A container to keep track of validated instances + * @param \SplObjectStorage $validatedInstancesContainer A container to keep track of validated instances * @return void * @api */ @@ -77,16 +77,17 @@ protected function isValid($object) $propertyValue = $this->getPropertyValue($object, $propertyName); $result = $this->checkProperty($propertyValue, $validators); if ($result !== null) { - $this->getResult()->forProperty($propertyName)->merge($result); + if ($this->getResult()) { + $this->getResult()->forProperty($propertyName)->merge($result); + } } } } /** - * @param $object * @return boolean */ - protected function isUninitializedProxy($object) + protected function isUninitializedProxy(object $object) { return ($object instanceof DoctrineProxy && $object->__isInitialized() === false); } @@ -132,7 +133,7 @@ protected function getPropertyValue($object, $propertyName) * found errors to the $messages object. * * @param mixed $value The value to be validated - * @param array $validators The validators to be called on the value + * @param array|\SplObjectStorage $validators The validators to be called on the value * @return NULL|ErrorResult */ protected function checkProperty($value, $validators) @@ -162,10 +163,12 @@ protected function checkProperty($value, $validators) * @return void * @api */ - public function addPropertyValidator($propertyName, ValidatorInterface $validator) + public function addPropertyValidator(string $propertyName, ValidatorInterface $validator) { if (!isset($this->propertyValidators[$propertyName])) { - $this->propertyValidators[$propertyName] = new \SplObjectStorage(); + /** @var \SplObjectStorage $storage */ + $storage = new \SplObjectStorage(); + $this->propertyValidators[$propertyName] = $storage; } $this->propertyValidators[$propertyName]->attach($validator); } @@ -173,13 +176,18 @@ public function addPropertyValidator($propertyName, ValidatorInterface $validato /** * Returns all property validators - or only validators of the specified property * - * @param string $propertyName Name of the property to return validators for - * @return array An array of validators + * @param ?string $propertyName Name of the property to return validators for + * @return ($propertyName is null ? array> : \SplObjectStorage) */ - public function getPropertyValidators($propertyName = null) + public function getPropertyValidators(?string $propertyName = null) { if ($propertyName !== null) { - return $this->propertyValidators[$propertyName] ?? []; + $propertyValidators = $this->propertyValidators[$propertyName] ?? null; + if (!$propertyValidators) { + /** @var \SplObjectStorage $propertyValidators */ + $propertyValidators = new \SplObjectStorage(); + } + return $propertyValidators; } return $this->propertyValidators; } diff --git a/Neos.Flow/Classes/Validation/Validator/MediaTypeValidator.php b/Neos.Flow/Classes/Validation/Validator/MediaTypeValidator.php index 5d0c7b3444..bc519d58c1 100644 --- a/Neos.Flow/Classes/Validation/Validator/MediaTypeValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/MediaTypeValidator.php @@ -32,7 +32,7 @@ class MediaTypeValidator extends AbstractValidator * 2 => type * 3 => required (boolean, optional) * - * @var array + * @var array */ protected $supportedOptions = [ 'allowedTypes' => [[], 'Array of allowed media ranges', 'array', true], diff --git a/Neos.Flow/Classes/Validation/Validator/NumberRangeValidator.php b/Neos.Flow/Classes/Validation/Validator/NumberRangeValidator.php index ba6c31be92..07a1db9858 100644 --- a/Neos.Flow/Classes/Validation/Validator/NumberRangeValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/NumberRangeValidator.php @@ -20,7 +20,7 @@ class NumberRangeValidator extends AbstractValidator { /** - * @var array + * @var array */ protected $supportedOptions = [ 'minimum' => [0, 'The minimum value to accept', 'integer'], diff --git a/Neos.Flow/Classes/Validation/Validator/NumberValidator.php b/Neos.Flow/Classes/Validation/Validator/NumberValidator.php index dd1a7db346..754a497cf9 100644 --- a/Neos.Flow/Classes/Validation/Validator/NumberValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/NumberValidator.php @@ -23,7 +23,7 @@ class NumberValidator extends AbstractValidator { /** - * @var array + * @var array */ protected $supportedOptions = [ 'locale' => [null, 'The locale to use for number parsing', 'string|Locale'], diff --git a/Neos.Flow/Classes/Validation/Validator/ObjectValidatorInterface.php b/Neos.Flow/Classes/Validation/Validator/ObjectValidatorInterface.php index 43f27b1a29..e1c3fa30d8 100644 --- a/Neos.Flow/Classes/Validation/Validator/ObjectValidatorInterface.php +++ b/Neos.Flow/Classes/Validation/Validator/ObjectValidatorInterface.php @@ -21,7 +21,7 @@ interface ObjectValidatorInterface extends ValidatorInterface /** * Allows to set a container to keep track of validated instances. * - * @param \SplObjectStorage $validatedInstancesContainer A container to keep track of validated instances + * @param \SplObjectStorage $validatedInstancesContainer A container to keep track of validated instances * @return void * @api */ diff --git a/Neos.Flow/Classes/Validation/Validator/RegularExpressionValidator.php b/Neos.Flow/Classes/Validation/Validator/RegularExpressionValidator.php index b2e7ad387b..18cbf52301 100644 --- a/Neos.Flow/Classes/Validation/Validator/RegularExpressionValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/RegularExpressionValidator.php @@ -21,7 +21,7 @@ class RegularExpressionValidator extends AbstractValidator { /** - * @var array + * @var array */ protected $supportedOptions = [ 'regularExpression' => ['', 'The regular expression to use for validation, used as given', 'string', true] diff --git a/Neos.Flow/Classes/Validation/Validator/StringLengthValidator.php b/Neos.Flow/Classes/Validation/Validator/StringLengthValidator.php index a48ecd7f19..e99f359400 100644 --- a/Neos.Flow/Classes/Validation/Validator/StringLengthValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/StringLengthValidator.php @@ -22,7 +22,7 @@ class StringLengthValidator extends AbstractValidator { /** - * @var array + * @var array */ protected $supportedOptions = [ 'minimum' => [0, 'Minimum length for a valid string', 'integer'], diff --git a/Neos.Flow/Classes/Validation/Validator/UniqueEntityValidator.php b/Neos.Flow/Classes/Validation/Validator/UniqueEntityValidator.php index 83fcb69dd2..8fba7bf2f7 100644 --- a/Neos.Flow/Classes/Validation/Validator/UniqueEntityValidator.php +++ b/Neos.Flow/Classes/Validation/Validator/UniqueEntityValidator.php @@ -16,7 +16,6 @@ use Neos\Flow\Reflection\ClassSchema; use Neos\Utility\ObjectAccess; use Neos\Flow\Reflection\ReflectionService; -use Neos\Utility\TypeHandling; use Neos\Flow\Validation\Exception\InvalidValidationOptionsException; /** @@ -39,7 +38,7 @@ class UniqueEntityValidator extends AbstractValidator protected $persistenceManager; /** - * @var array + * @var array */ protected $supportedOptions = [ 'identityProperties' => [null, 'List of custom identity properties.', 'array'] @@ -60,7 +59,7 @@ protected function isValid($value) throw new InvalidValidationOptionsException('The value supplied for the UniqueEntityValidator must be an object.', 1358454270); } - $classSchema = $this->reflectionService->getClassSchema(TypeHandling::getTypeForValue($value)); + $classSchema = $this->reflectionService->getClassSchema($value); if ($classSchema === null || $classSchema->getModelType() !== ClassSchema::MODELTYPE_ENTITY) { throw new InvalidValidationOptionsException('The object supplied for the UniqueEntityValidator must be an entity.', 1358454284); } diff --git a/Neos.Flow/Classes/Validation/Validator/ValidatorInterface.php b/Neos.Flow/Classes/Validation/Validator/ValidatorInterface.php index b142149381..55cd6c4228 100644 --- a/Neos.Flow/Classes/Validation/Validator/ValidatorInterface.php +++ b/Neos.Flow/Classes/Validation/Validator/ValidatorInterface.php @@ -41,7 +41,7 @@ public function validate($value); /** * Returns the options of this validator which can be specified in the constructor * - * @return array + * @return array */ public function getOptions(); } diff --git a/Neos.Flow/Classes/Validation/ValidatorResolver.php b/Neos.Flow/Classes/Validation/ValidatorResolver.php index 3400c85715..11a72dffae 100644 --- a/Neos.Flow/Classes/Validation/ValidatorResolver.php +++ b/Neos.Flow/Classes/Validation/ValidatorResolver.php @@ -77,7 +77,7 @@ class ValidatorResolver protected $reflectionService; /** - * @var array + * @var array */ protected $baseValidatorConjunctions = []; @@ -87,7 +87,7 @@ class ValidatorResolver * could be resolved. * * @param string $validatorType Either one of the built-in data types or fully qualified validator class name - * @param array $validatorOptions Options to be passed to the validator + * @param array $validatorOptions Options to be passed to the validator * @return ValidatorInterface|null * @throws Exception\NoSuchValidatorException * @throws Exception\InvalidValidationConfigurationException @@ -114,10 +114,6 @@ public function createValidator($validatorType, array $validatorOptions = []) throw new Exception\NoSuchValidatorException('The validator "' . $validatorObjectName . '" is not of scope singleton or prototype!', 1300694835); } - if (!($validator instanceof ValidatorInterface)) { - throw new Exception\NoSuchValidatorException(sprintf('The validator "%s" does not implement %s!', $validatorObjectName, ValidatorInterface::class), 1300694875); - } - return $validator; } @@ -127,7 +123,7 @@ public function createValidator($validatorType, array $validatorOptions = []) * If no validation is necessary, the returned validator is empty. * * @param string $targetClassName Fully qualified class name of the target class, ie. the class which should be validated - * @param array $validationGroups The validation groups to build the validator for + * @param array $validationGroups The validation groups to build the validator for * @return ConjunctionValidator The validator conjunction * @throws Exception\InvalidValidationConfigurationException * @throws Exception\InvalidValidationOptionsException @@ -150,10 +146,10 @@ public function getBaseValidatorConjunction($targetClassName, array $validationG * - by the data type specified in the param annotations * - additional validators specified in the validate annotations of a method * - * @param string $className + * @param class-string $className * @param string $methodName - * @param array $methodParameters Optional pre-compiled array of method parameters - * @param array $methodValidateAnnotations Optional pre-compiled array of validate annotations (as array) + * @param array|null $methodParameters Optional pre-compiled array of method parameters + * @param array, argumentName: string}>|null $methodValidateAnnotations Optional pre-compiled array of validate annotations (as array) * @return array An Array of ValidatorConjunctions for each method parameters. * @throws Exception\InvalidValidationConfigurationException * @throws Exception\NoSuchValidatorException @@ -202,7 +198,11 @@ public function buildMethodArgumentsValidatorConjunctions($className, $methodNam } foreach ($methodValidateAnnotations as $annotationParameters) { - $newValidator = $this->createValidator($annotationParameters['type'], $annotationParameters['options']); + $validatorType = $annotationParameters['type'] ?? null; + if ($validatorType === null) { + throw new \Exception('Missing validator type', 1743872674); + } + $newValidator = $this->createValidator($validatorType, $annotationParameters['options']); if ($newValidator === null) { throw new Exception\NoSuchValidatorException('Invalid validate annotation in ' . $className . '->' . $methodName . '(): Could not resolve class name for validator "' . $annotationParameters['type'] . '".', 1239853109); } @@ -235,7 +235,7 @@ public function reset() * Builds a chain of nested object validators by specification of the given * object path. * - * @param array $objectPath The object path + * @param array $objectPath The object path * @param ValidatorInterface $propertyValidator The validator which should be added to the property specified by objectPath * @return GenericObjectValidator * @throws Exception\InvalidValidationOptionsException @@ -252,7 +252,12 @@ protected function buildSubObjectValidator(array $objectPath, ValidatorInterface $parentObjectValidator = $subObjectValidator; } - $parentObjectValidator->addPropertyValidator(array_shift($objectPath), $propertyValidator); + $propertyName = array_shift($objectPath); + if (!is_string($propertyName)) { + throw new \Exception('Missing propertyName', 1743872593); + } + + $parentObjectValidator->addPropertyValidator($propertyName, $propertyValidator); return $rootObjectValidator; } @@ -275,7 +280,7 @@ protected function buildSubObjectValidator(array $objectPath, ValidatorInterface * * @param string $indexKey The key to use as index in $this->baseValidatorConjunctions; calculated from target class name and validation groups * @param string $targetClassName The data type to build the validation conjunction for. Needs to be the fully qualified class name. - * @param array $validationGroups The validation groups to build the validator for + * @param array $validationGroups The validation groups to build the validator for * @return void * @throws Exception\NoSuchValidatorException * @throws \InvalidArgumentException @@ -333,6 +338,9 @@ protected function buildBaseValidatorConjunction($indexKey, $targetClassName, ar $validateAnnotations = $this->reflectionService->getPropertyAnnotations($targetClassName, $classPropertyName, Flow\Validate::class); foreach ($validateAnnotations as $validateAnnotation) { + if ($validateAnnotation->type === null) { + continue; + } if ($validateAnnotation->type === 'Collection') { $needsCollectionValidator = false; $validateAnnotation->options = array_merge(['elementType' => $parsedType['elementType'], 'validationGroups' => $validationGroups], $validateAnnotation->options); @@ -356,7 +364,9 @@ protected function buildBaseValidatorConjunction($indexKey, $targetClassName, ar if ($needsCollectionValidator) { $collectionValidator = $this->createValidator(Validator\CollectionValidator::class, ['elementType' => $parsedType['elementType'], 'validationGroups' => $validationGroups]); - $objectValidator->addPropertyValidator($classPropertyName, $collectionValidator); + if ($collectionValidator instanceof ValidatorInterface) { + $objectValidator->addPropertyValidator($classPropertyName, $collectionValidator); + } } if ($needsObjectValidator) { $validatorForProperty = $this->getBaseValidatorConjunction($propertyTargetClassName, $validationGroups); @@ -424,7 +434,7 @@ protected function addCustomValidators($targetClassName, ConjunctionValidator &$ * Returns a map of object validator class names. * * @param ObjectManagerInterface $objectManager - * @return array Array of object validator class names + * @return list> Array of object validator class names * @Flow\CompileStatic */ public static function getPolyTypeObjectValidatorImplementationClassNames($objectManager) @@ -439,7 +449,7 @@ public static function getPolyTypeObjectValidatorImplementationClassNames($objec * validator is available false is returned * * @param string $validatorType Either the fully qualified class name of the validator or the short name of a built-in validator - * @return string|false Class name of the validator or false if not available + * @return class-string|false Class name of the validator or false if not available */ protected function resolveValidatorObjectName($validatorType) { @@ -448,6 +458,7 @@ protected function resolveValidatorObjectName($validatorType) $validatorClassNames = static::getValidatorImplementationClassNames($this->objectManager); if ($this->objectManager->isRegistered($validatorType) && isset($validatorClassNames[$validatorType])) { + /** @var class-string $validatorType */ return $validatorType; } @@ -458,6 +469,7 @@ protected function resolveValidatorObjectName($validatorType) $possibleClassName = sprintf('Neos\Flow\Validation\Validator\%sValidator', $this->getValidatorType($validatorType)); } if ($this->objectManager->isRegistered($possibleClassName) && isset($validatorClassNames[$possibleClassName])) { + /** @var class-string $possibleClassName */ return $possibleClassName; } @@ -468,7 +480,7 @@ protected function resolveValidatorObjectName($validatorType) * Returns all class names implementing the ValidatorInterface. * * @param ObjectManagerInterface $objectManager - * @return array Array of class names implementing ValidatorInterface indexed by class name + * @return array,int> Array of class names implementing ValidatorInterface indexed by class name * @Flow\CompileStatic */ public static function getValidatorImplementationClassNames($objectManager) diff --git a/Neos.Flow/Tests/Unit/Core/Booting/ScriptsTest.php b/Neos.Flow/Tests/Unit/Core/Booting/ScriptsTest.php index 3088f3fe4a..d3daf2e79a 100644 --- a/Neos.Flow/Tests/Unit/Core/Booting/ScriptsTest.php +++ b/Neos.Flow/Tests/Unit/Core/Booting/ScriptsTest.php @@ -27,11 +27,11 @@ */ class ScriptsMock extends Scripts { - protected static function ensureCLISubrequestsUseCurrentlyRunningPhpBinary($phpBinaryPathAndFilename) + protected static function ensureCLISubrequestsUseCurrentlyRunningPhpBinary($phpBinaryPathAndFilename): void { } - protected static function ensureWebSubrequestsUseCurrentlyRunningPhpVersion($phpCommand) + protected static function ensureWebSubrequestsUseCurrentlyRunningPhpVersion($phpCommand): void { } diff --git a/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationTest.php b/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationTest.php index d7909b649a..bfb74849cd 100644 --- a/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationTest.php +++ b/Neos.Flow/Tests/Unit/ObjectManagement/Configuration/ConfigurationTest.php @@ -119,10 +119,10 @@ public function setFactoryMethodNameAcceptsValidStrings() /** * @test */ - public function setFactoryMethodNameRejectsAnythingElseThanAString() + public function setFactoryMethodNameRejectsEmptyString() { $this->expectException(\InvalidArgumentException::class); - $this->objectConfiguration->setFactoryMethodName([]); + $this->objectConfiguration->setFactoryMethodName(''); } /** diff --git a/Neos.FluidAdaptor/Classes/Core/Cache/CacheAdaptor.php b/Neos.FluidAdaptor/Classes/Core/Cache/CacheAdaptor.php index 43bb16e5a4..a80d47190b 100644 --- a/Neos.FluidAdaptor/Classes/Core/Cache/CacheAdaptor.php +++ b/Neos.FluidAdaptor/Classes/Core/Cache/CacheAdaptor.php @@ -50,7 +50,7 @@ public function get($name) * @param string $name * @param string $value */ - public function set($name, $value) + public function set($name, $value): void { // we need to strip the first line with the php header as the flow cache adds that again. $this->flowCache->set($name, substr($value, strpos($value, "\n") + 1)); diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php b/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php index 286870a6ac..9334df24ad 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php @@ -97,7 +97,7 @@ public function process(NodeInterface $node, $interceptorPosition, ParsingState if (strpos($node->getText(), 'Public/') === false) { return $node; } - $textParts = preg_split(self::PATTERN_SPLIT_AT_RESOURCE_URIS, $node->getText(), -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + $textParts = preg_split(self::PATTERN_SPLIT_AT_RESOURCE_URIS, $node->getText(), -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) ?: []; $node = new RootNode(); foreach ($textParts as $part) { $matches = []; @@ -109,7 +109,7 @@ public function process(NodeInterface $node, $interceptorPosition, ParsingState if ($this->defaultPackageKey !== null) { $arguments['package'] = new TextNode($this->defaultPackageKey); } - if (isset($matches['Package']) && FlowPackageKey::isPackageKeyValid($matches['Package'])) { + if (FlowPackageKey::isPackageKeyValid($matches['Package'])) { $arguments['package'] = new TextNode($matches['Package']); } @@ -127,7 +127,7 @@ public function process(NodeInterface $node, $interceptorPosition, ParsingState /** * This interceptor wants to hook into text nodes. * - * @return array Array of INTERCEPT_* constants + * @return array Array of INTERCEPT_* constants */ public function getInterceptionPoints() { diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/Expression/LegacyNamespaceExpressionNode.php b/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/Expression/LegacyNamespaceExpressionNode.php index 1af2886355..cd63f4f625 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/Expression/LegacyNamespaceExpressionNode.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/Expression/LegacyNamespaceExpressionNode.php @@ -24,14 +24,14 @@ class LegacyNamespaceExpressionNode extends AbstractExpressionNode implements Ex * Pattern which detects namespace declarations made inline. * syntax, e.g. {namespace neos=TYPO3\Neos\ViewHelpers}. */ - public static $detectionExpression = '/{namespace\\s*([a-z0-9]+)\\s*=\\s*([a-z0-9_\\\\]+)\\s*}/i'; + public static string $detectionExpression = '/{namespace\\s*([a-z0-9]+)\\s*=\\s*([a-z0-9_\\\\]+)\\s*}/i'; /** * Evaluates the expression stored in this node, in the context of $renderingcontext. * * @param RenderingContextInterface $renderingContext * @param string $expression - * @param array $matches + * @param array $matches * @return mixed */ public static function evaluateExpression(RenderingContextInterface $renderingContext, $expression, array $matches) diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/ResourceUriNode.php b/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/ResourceUriNode.php index db44103330..81301ef0d4 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/ResourceUriNode.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/ResourceUriNode.php @@ -35,18 +35,17 @@ class ResourceUriNode extends ViewHelperNode protected $viewHelperResolver; /** - * @var string + * @var class-string */ protected $viewHelperClassName = ResourceViewHelper::class; /** * @param ViewHelperResolver $viewHelperResolver */ - public function injectViewHelperResolver(ViewHelperResolver $viewHelperResolver) + public function injectViewHelperResolver(ViewHelperResolver $viewHelperResolver): void { $this->viewHelperResolver = $viewHelperResolver; $this->uninitializedViewHelper = $this->viewHelperResolver->createViewHelperInstanceFromClassName($this->viewHelperClassName); - /** @phpstan-ignore-next-line we use internal api */ $this->uninitializedViewHelper->setViewHelperNode($this); $this->argumentDefinitions = $this->viewHelperResolver->getArgumentDefinitionsForViewHelper($this->uninitializedViewHelper); } diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/TemplateParser.php b/Neos.FluidAdaptor/Classes/Core/Parser/TemplateParser.php index 1ad1054885..db67139b41 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/TemplateParser.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/TemplateParser.php @@ -17,7 +17,7 @@ public function isEscapingEnabled() /** * @param boolean $escapingEnabled */ - public function setEscapingEnabled($escapingEnabled) + public function setEscapingEnabled($escapingEnabled): void { $this->escapingEnabled = $escapingEnabled; } diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/TemplateProcessor/EscapingFlagProcessor.php b/Neos.FluidAdaptor/Classes/Core/Parser/TemplateProcessor/EscapingFlagProcessor.php index fbdc9a72a1..1b61da951e 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/TemplateProcessor/EscapingFlagProcessor.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/TemplateProcessor/EscapingFlagProcessor.php @@ -25,12 +25,12 @@ class EscapingFlagProcessor implements TemplateProcessorInterface */ protected $renderingContext; - public static $SCAN_PATTERN_ESCAPINGMODIFIER = '/{escapingEnabled\s*=\s*(?Ptrue|false)\s*}/i'; + public static string $SCAN_PATTERN_ESCAPINGMODIFIER = '/{escapingEnabled\s*=\s*(?Ptrue|false)\s*}/i'; /** * @param RenderingContextInterface $renderingContext */ - public function setRenderingContext(RenderingContextInterface $renderingContext) + public function setRenderingContext(RenderingContextInterface $renderingContext): void { $this->renderingContext = $renderingContext; } @@ -56,7 +56,7 @@ public function preProcessSource($templateSource) if (strtolower($matches[0]['enabled']) === 'false') { $this->renderingContext->getTemplateParser()->setEscapingEnabled(false); } - $templateSource = preg_replace(self::$SCAN_PATTERN_ESCAPINGMODIFIER, '', $templateSource); + $templateSource = preg_replace(self::$SCAN_PATTERN_ESCAPINGMODIFIER, '', $templateSource) ?: ''; return $templateSource; } diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/TemplateProcessor/NamespaceDetectionTemplateProcessor.php b/Neos.FluidAdaptor/Classes/Core/Parser/TemplateProcessor/NamespaceDetectionTemplateProcessor.php index b6c18f2664..3c813c0c01 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/TemplateProcessor/NamespaceDetectionTemplateProcessor.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/TemplateProcessor/NamespaceDetectionTemplateProcessor.php @@ -29,7 +29,7 @@ class NamespaceDetectionTemplateProcessor extends FluidNamespaceDetectionTemplat /** * Extension of the default pattern for dynamic tags including namespaces with uppercase letters. */ - public static $EXTENDED_SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS = '/ + public static string $EXTENDED_SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS = '/ ( (?: <\/? # Start dynamic tags (?:(?:[a-zA-Z0-9\\.]*):[a-zA-Z0-9\\.]+) # A tag consists of the namespace prefix and word characters @@ -75,7 +75,7 @@ public function preProcessSource($templateSource) */ public function protectCDataSectionsFromParser(string $templateSource) { - $parts = preg_split('/(\<\!\[CDATA\[|\]\]\>)/', $templateSource, -1, PREG_SPLIT_DELIM_CAPTURE); + $parts = preg_split('/(\<\!\[CDATA\[|\]\]\>)/', $templateSource, -1, PREG_SPLIT_DELIM_CAPTURE) ?: []; $balance = 0; $content = ''; $resultingParts = []; @@ -127,7 +127,7 @@ public function protectCDataSectionsFromParser(string $templateSource) public function throwExceptionsForUnhandledNamespaces(string $templateSource): void { $viewHelperResolver = $this->renderingContext->getViewHelperResolver(); - $splitTemplate = preg_split(static::$EXTENDED_SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS, $templateSource, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $splitTemplate = preg_split(static::$EXTENDED_SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS, $templateSource, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY) ?: []; foreach ($splitTemplate as $templateElement) { if (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG, $templateElement, $matchedVariables) > 0) { if (!$viewHelperResolver->isNamespaceValidOrIgnored($matchedVariables['NamespaceIdentifier'])) { @@ -148,11 +148,9 @@ public function throwExceptionsForUnhandledNamespaces(string $templateSource): v foreach ($sections as $section) { if (preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS, $section, $matchedVariables) > 0) { preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $section, $shorthandViewHelpers, PREG_SET_ORDER); - if (is_array($shorthandViewHelpers) === true) { - foreach ($shorthandViewHelpers as $shorthandViewHelper) { - if (!$viewHelperResolver->isNamespaceValidOrIgnored($shorthandViewHelper['NamespaceIdentifier'])) { - throw new UnknownNamespaceException('Unknown Namespace: ' . $shorthandViewHelper['NamespaceIdentifier']); - } + foreach ($shorthandViewHelpers as $shorthandViewHelper) { + if (!$viewHelperResolver->isNamespaceValidOrIgnored($shorthandViewHelper['NamespaceIdentifier'])) { + throw new UnknownNamespaceException('Unknown Namespace: ' . $shorthandViewHelper['NamespaceIdentifier']); } } } diff --git a/Neos.FluidAdaptor/Classes/Core/Rendering/RenderingContext.php b/Neos.FluidAdaptor/Classes/Core/Rendering/RenderingContext.php index 3c68fb552b..8869960689 100644 --- a/Neos.FluidAdaptor/Classes/Core/Rendering/RenderingContext.php +++ b/Neos.FluidAdaptor/Classes/Core/Rendering/RenderingContext.php @@ -15,7 +15,6 @@ use Neos\FluidAdaptor\Core\Parser\TemplateParser; use Neos\FluidAdaptor\Core\Parser\TemplateProcessor\EscapingFlagProcessor; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\FluidAdaptor\Core\Parser\Interceptor\ResourceInterceptor; @@ -25,6 +24,7 @@ use Neos\FluidAdaptor\Core\ViewHelper\ViewHelperResolver; use Neos\FluidAdaptor\View\TemplatePaths; use TYPO3Fluid\Fluid\Core\Parser\Configuration; +use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\AbstractExpressionNode; use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\CastingExpressionNode; use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\MathExpressionNode; use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\TernaryExpressionNode; @@ -42,7 +42,7 @@ class RenderingContext extends FluidRenderingContext implements FlowAwareRenderi * which will be consulted when an expression does not match * any built-in parser expression types. * - * @var array + * @var array> */ protected $expressionNodeTypes = [ LegacyNamespaceExpressionNode::class, @@ -81,7 +81,7 @@ class RenderingContext extends FluidRenderingContext implements FlowAwareRenderi /** * RenderingContext constructor. * - * @param array $options + * @param array $options */ public function __construct(array $options = []) { @@ -100,7 +100,7 @@ public function __construct(array $options = []) /** * @param ObjectManagerInterface $objectManager */ - public function injectObjectManager(ObjectManagerInterface $objectManager) + public function injectObjectManager(ObjectManagerInterface $objectManager): void { $this->objectManager = $objectManager; } @@ -116,11 +116,11 @@ public function getControllerContext() /** * @param ControllerContext $controllerContext */ - public function setControllerContext($controllerContext) + public function setControllerContext($controllerContext): void { $this->controllerContext = $controllerContext; $request = $controllerContext->getRequest(); - if (!$this->templatePaths instanceof TemplatePaths || !$request instanceof ActionRequest) { + if (!$this->templatePaths instanceof TemplatePaths) { return; } diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractConditionViewHelper.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractConditionViewHelper.php index 052ef09e98..60c8b848f9 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractConditionViewHelper.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractConditionViewHelper.php @@ -46,7 +46,7 @@ abstract class AbstractConditionViewHelper extends AbstractViewHelper /** * Initializes the "then" and "else" arguments */ - public function initializeArguments() + public function initializeArguments(): void { $this->registerArgument('then', 'mixed', 'Value to be returned if the condition if met.', false); $this->registerArgument('else', 'mixed', 'Value to be returned if the condition if not met.', false); @@ -66,18 +66,19 @@ public function initializeArguments() * subclasses that will be using this base class in the future. Let this * be a warning if someone considers changing this method signature! * - * @param array|NULL $arguments + * @param array|NULL $arguments * @param RenderingContextInterface $renderingContext * @return boolean * @api */ protected static function evaluateCondition($arguments, RenderingContextInterface $renderingContext) { - return (boolean)$arguments['condition']; + // fallback value derived from registerArgument default value + return (boolean)($arguments['condition'] ?? false); } /** - * @param array $arguments + * @param array $arguments * @param \Closure $renderChildrenClosure * @param RenderingContextInterface $renderingContext * @return mixed @@ -103,8 +104,8 @@ public static function renderStatic(array $arguments, \Closure $renderChildrenCl } /** - * @param array $closures - * @param array $conditionClosures + * @param array $closures + * @param array $conditionClosures * @param RenderingContextInterface $renderingContext * @return string */ @@ -236,7 +237,7 @@ public function compile($argumentsName, $closureName, &$initializationPhpCode, V /** * @param boolean $isConditionFullfilled - * @param array $arguments + * @param array $arguments * @param RenderingContextInterface $renderingContext * @return mixed */ diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractLocaleAwareViewHelper.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractLocaleAwareViewHelper.php index 5f33d1e651..436817dfea 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractLocaleAwareViewHelper.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractLocaleAwareViewHelper.php @@ -39,7 +39,7 @@ public function __construct() /** * @param I18n\Service $localizationService */ - public function injectLocalizationService(I18n\Service $localizationService) + public function injectLocalizationService(I18n\Service $localizationService): void { $this->localizationService = $localizationService; } diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractTagBasedViewHelper.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractTagBasedViewHelper.php index d4743808a6..2ce96b048d 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractTagBasedViewHelper.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractTagBasedViewHelper.php @@ -32,7 +32,7 @@ abstract class AbstractTagBasedViewHelper extends AbstractViewHelper /** * Names of all registered tag attributes * - * @var array + * @var array */ private static $tagAttributes = []; @@ -156,7 +156,7 @@ protected function registerUniversalTagAttributes() * which in the default implementation will throw an error * about "undeclared argument used". * - * @param array $arguments + * @param array|array|null> $arguments * @return void */ public function handleAdditionalArguments(array $arguments) diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractViewHelper.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractViewHelper.php index 6731a63562..e80a9b38de 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractViewHelper.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractViewHelper.php @@ -59,9 +59,7 @@ public function setRenderingContext(RenderingContextInterface $renderingContext) $this->renderingContext = $renderingContext; $this->templateVariableContainer = $renderingContext->getVariableProvider(); $this->viewHelperVariableContainer = $renderingContext->getViewHelperVariableContainer(); - if ($renderingContext instanceof FlowAwareRenderingContextInterface) { - $this->controllerContext = $renderingContext->getControllerContext(); - } + $this->controllerContext = $renderingContext->getControllerContext(); } /** diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php index 8351bfa4f1..9282263018 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php @@ -15,6 +15,7 @@ use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Flow\Package\Package; use Neos\Flow\Package\PackageManager; +use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface; /** * Class ViewHelperResolver @@ -49,17 +50,17 @@ class ViewHelperResolver extends \TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperRes * will look for classes in both namespaces starting * from the bottom. * - * @var array + * @var array */ protected $namespaces = []; /** * @Flow\InjectConfiguration(path="namespaces") - * @var array + * @var array */ protected $namespacesFromConfiguration; - public function initializeObject($reason) + public function initializeObject(int $reason): void { if ($reason === ObjectManagerInterface::INITIALIZATIONCAUSE_RECREATED) { return; @@ -83,8 +84,9 @@ public function initializeObject($reason) } /** - * @param string $viewHelperClassName - * @return \TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface + * @template T of ViewHelperInterface + * @param class-string $viewHelperClassName + * @return T */ public function createViewHelperInstanceFromClassName($viewHelperClassName) { @@ -125,7 +127,7 @@ public function createViewHelperInstanceFromClassName($viewHelperClassName) * when you use this method you should always include the "f" namespace. * * @param string $identifier - * @param string|array $phpNamespace + * @param string|array|null $phpNamespace * @return void */ public function addNamespace($identifier, $phpNamespace) @@ -149,7 +151,7 @@ public function addNamespace($identifier, $phpNamespace) * @param string $identifier * @param string $phpNamespace */ - protected function addNamespaceInternal($identifier, $phpNamespace) + protected function addNamespaceInternal($identifier, $phpNamespace): void { if (!isset($this->namespaces[$identifier])) { $this->namespaces[$identifier] = []; diff --git a/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetController.php b/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetController.php index c7b38b4a80..4c07eab1e2 100644 --- a/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetController.php +++ b/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetController.php @@ -36,7 +36,7 @@ abstract class AbstractWidgetController extends ActionController /** * Configuration for this widget. * - * @var array + * @var array * @api */ protected $widgetConfiguration; @@ -60,7 +60,6 @@ abstract class AbstractWidgetController extends ActionController */ public function processRequest(ActionRequest $request): ResponseInterface { - /** @var WidgetContext $widgetContext */ $widgetContext = $request->getInternalArgument('__widgetContext'); if (!$widgetContext instanceof WidgetContext) { throw new WidgetContextNotFoundException('The widget context could not be found in the request.', 1307450180); diff --git a/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetViewHelper.php b/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetViewHelper.php index e390a32db6..db341c1186 100644 --- a/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetViewHelper.php +++ b/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetViewHelper.php @@ -20,6 +20,7 @@ use Neos\FluidAdaptor\Core\ViewHelper\AbstractViewHelper; use Neos\FluidAdaptor\Core\ViewHelper\Facets\ChildNodeAccessInterface; use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler; +use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface; use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode; use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode; @@ -38,7 +39,7 @@ abstract class AbstractWidgetViewHelper extends AbstractViewHelper implements Ch * This needs to be filled by the individual subclass using * property injection. * - * @var AbstractWidgetController + * @var AbstractWidgetController|DependencyProxy * @api */ protected $controller; @@ -150,7 +151,7 @@ private function initializeWidgetContext() * Stores the syntax tree child nodes in the Widget Context, so they can be * rendered with lateron. * - * @param array $childNodes The SyntaxTree Child nodes of this ViewHelper. + * @param array $childNodes The SyntaxTree Child nodes of this ViewHelper. * @return void */ public function setChildNodes(array $childNodes) @@ -166,7 +167,7 @@ public function setChildNodes(array $childNodes) /** * Generate the configuration for this widget. Override to adjust. * - * @return array + * @return array * @api */ protected function getWidgetConfiguration() @@ -179,7 +180,7 @@ protected function getWidgetConfiguration() * * By default, returns getWidgetConfiguration(). Should become API later. * - * @return array + * @return array */ protected function getAjaxWidgetConfiguration() { @@ -191,7 +192,7 @@ protected function getAjaxWidgetConfiguration() * * By default, returns getWidgetConfiguration(). Should become API later. * - * @return array + * @return array */ protected function getNonAjaxWidgetConfiguration() { @@ -231,6 +232,9 @@ protected function initiateSubRequest() } $subRequest->setControllerObjectName($this->widgetContext->getControllerObjectName()); try { + if (!($this->controller instanceof AbstractWidgetController)) { + throw new Exception\MissingControllerException('initiateSubRequest() can not be called if there is no controller inside $this->controller. Make sure to add the @Neos\Flow\Annotations\Inject annotation in your widget class.', 1284401632); + } $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. diff --git a/Neos.FluidAdaptor/Classes/Core/Widget/AjaxWidgetContextHolder.php b/Neos.FluidAdaptor/Classes/Core/Widget/AjaxWidgetContextHolder.php index f78a163c2f..81a7077866 100644 --- a/Neos.FluidAdaptor/Classes/Core/Widget/AjaxWidgetContextHolder.php +++ b/Neos.FluidAdaptor/Classes/Core/Widget/AjaxWidgetContextHolder.php @@ -30,7 +30,7 @@ class AjaxWidgetContextHolder * An array $ajaxWidgetIdentifier => $widgetContext * which stores the widget context. * - * @var array + * @var array */ protected $widgetContexts = []; diff --git a/Neos.FluidAdaptor/Classes/Core/Widget/WidgetContext.php b/Neos.FluidAdaptor/Classes/Core/Widget/WidgetContext.php index de1beed414..fb679f500f 100644 --- a/Neos.FluidAdaptor/Classes/Core/Widget/WidgetContext.php +++ b/Neos.FluidAdaptor/Classes/Core/Widget/WidgetContext.php @@ -48,7 +48,7 @@ class WidgetContext * controller as $this->widgetConfiguration, if being inside an AJAX * request * - * @var array + * @var array */ protected $ajaxWidgetConfiguration; @@ -57,7 +57,7 @@ class WidgetContext * controller as $this->widgetConfiguration, if being inside a non-AJAX * request * - * @var array + * @var array */ protected $nonAjaxWidgetConfiguration; /** @@ -71,7 +71,7 @@ class WidgetContext * The child nodes of the Widget ViewHelper. * Only available inside non-AJAX requests. * - * @var RootNode + * @var ?RootNode * @Flow\Transient */ protected $viewHelperChildNodes; @@ -80,7 +80,7 @@ class WidgetContext * The rendering context of the ViewHelperChildNodes. * Only available inside non-AJAX requests. * - * @var RenderingContextInterface + * @var ?RenderingContextInterface * @Flow\Transient */ protected $viewHelperChildNodeRenderingContext; @@ -120,7 +120,7 @@ public function setAjaxWidgetIdentifier($ajaxWidgetIdentifier) } /** - * @return array + * @return array */ public function getWidgetConfiguration() { @@ -132,7 +132,7 @@ public function getWidgetConfiguration() } /** - * @param array $ajaxWidgetConfiguration + * @param array $ajaxWidgetConfiguration * @return void */ public function setAjaxWidgetConfiguration(array $ajaxWidgetConfiguration) @@ -141,7 +141,7 @@ public function setAjaxWidgetConfiguration(array $ajaxWidgetConfiguration) } /** - * @param array $nonAjaxWidgetConfiguration + * @param array $nonAjaxWidgetConfiguration * @return void */ public function setNonAjaxWidgetConfiguration(array $nonAjaxWidgetConfiguration) @@ -178,17 +178,14 @@ public function setViewHelperChildNodes(RootNode $viewHelperChildNodes, Renderin } /** - * @return RootNode + * @return ?RootNode */ public function getViewHelperChildNodes() { return $this->viewHelperChildNodes; } - /** - * @return RenderingContextInterface - */ - public function getViewHelperChildNodeRenderingContext() + public function getViewHelperChildNodeRenderingContext(): ?RenderingContextInterface { return $this->viewHelperChildNodeRenderingContext; } diff --git a/Neos.FluidAdaptor/Classes/Service/AbstractGenerator.php b/Neos.FluidAdaptor/Classes/Service/AbstractGenerator.php index 0b37087aab..6001780143 100644 --- a/Neos.FluidAdaptor/Classes/Service/AbstractGenerator.php +++ b/Neos.FluidAdaptor/Classes/Service/AbstractGenerator.php @@ -54,7 +54,7 @@ public function __construct() * Get all class names inside this namespace and return them as array. * * @param string $namespace - * @return array Array of all class names inside a given namespace. + * @return array> Array of all class names inside a given namespace. */ protected function getClassNamesInNamespace($namespace) { @@ -101,7 +101,7 @@ protected function getTagNameForClass($className, $namespace) * @param \SimpleXMLElement $parentXmlNode Parent XML Node to add the child to * @param string $childNodeName Name of the child node * @param string $childNodeValue Value of the child node. Will be placed inside CDATA. - * @return \SimpleXMLElement the new element + * @return ?\SimpleXMLElement the new element */ protected function addChildWithCData(\SimpleXMLElement $parentXmlNode, $childNodeName, $childNodeValue) { @@ -110,8 +110,12 @@ protected function addChildWithCData(\SimpleXMLElement $parentXmlNode, $childNod $childNode = $domDocument->appendChild($domDocument->createElement($childNodeName)); $childNode->appendChild($domDocument->createCDATASection($childNodeValue)); - $childNodeTarget = $parentDomNode->ownerDocument->importNode($childNode, true); - $parentDomNode->appendChild($childNodeTarget); - return simplexml_import_dom($childNodeTarget); + $childNodeTarget = $parentDomNode->ownerDocument?->importNode($childNode, true); + if ($childNodeTarget instanceof \DOMNode) { + $parentDomNode->appendChild($childNodeTarget); + return simplexml_import_dom($childNodeTarget); + } + + return null; } } diff --git a/Neos.FluidAdaptor/Classes/Service/XsdGenerator.php b/Neos.FluidAdaptor/Classes/Service/XsdGenerator.php index c67220d2e9..be4fda8617 100644 --- a/Neos.FluidAdaptor/Classes/Service/XsdGenerator.php +++ b/Neos.FluidAdaptor/Classes/Service/XsdGenerator.php @@ -54,7 +54,11 @@ public function generateXsd($viewHelperNamespace, $xsdNamespace) $this->generateXmlForClassName($className, $viewHelperNamespace, $xmlRootNode); } - return $xmlRootNode->asXML(); + $result = $xmlRootNode->asXML(); + + return is_string($result) + ? $result + : throw new \Exception('Failed to generate xsd', 1743851104); } /** @@ -75,15 +79,22 @@ protected function generateXmlForClassName($className, $viewHelperNamespace, \Si $tagName = $this->getTagNameForClass($className, $viewHelperNamespace); $xsdElement = $xmlRootNode->addChild('xsd:element'); + /** @phpstan-ignore argument.type (offsetSet accepts mixed) */ $xsdElement->offsetSet('name', $tagName); - $this->docCommentParser->parseDocComment($reflectionClass->getDocComment()); + $docComment = $reflectionClass->getDocComment(); + if ($docComment !== false) { + $this->docCommentParser->parseDocComment($docComment); + } $this->addDocumentation($this->docCommentParser->getDescription(), $xsdElement); $xsdComplexType = $xsdElement->addChild('xsd:complexType'); + /** @phpstan-ignore argument.type (offsetSet accepts mixed) */ $xsdComplexType->offsetSet('mixed', 'true'); $xsdSequence = $xsdComplexType->addChild('xsd:sequence'); $xsdAny = $xsdSequence->addChild('xsd:any'); + /** @phpstan-ignore argument.type (offsetSet accepts mixed) */ $xsdAny->offsetSet('minOccurs', '0'); + /** @phpstan-ignore argument.type (offsetSet accepts mixed) */ $xsdAny->offsetSet('maxOccurs', 'unbounded'); $this->addAttributes($className, $xsdComplexType); @@ -106,10 +117,13 @@ protected function addAttributes($className, \SimpleXMLElement $xsdElement) foreach ($argumentDefinitions as $argumentDefinition) { /** @var \SimpleXMLElement $xsdAttribute */ $xsdAttribute = $xsdElement->addChild('xsd:attribute'); + /** @phpstan-ignore argument.type (offsetSet accepts mixed) */ $xsdAttribute->offsetSet('type', 'xsd:string'); + /** @phpstan-ignore argument.type (offsetSet accepts mixed) */ $xsdAttribute->offsetSet('name', $argumentDefinition->getName()); $this->addDocumentation($argumentDefinition->getDescription(), $xsdAttribute); if ($argumentDefinition->isRequired()) { + /** @phpstan-ignore argument.type (offsetSet accepts mixed) */ $xsdAttribute->offsetSet('use', 'required'); } } diff --git a/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php b/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php index 37a64bfa17..a16e5366b3 100644 --- a/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php +++ b/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php @@ -12,7 +12,6 @@ */ use Neos\FluidAdaptor\Exception; -use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Mvc\View\ViewInterface; use Neos\FluidAdaptor\Core\Rendering\RenderingContext; @@ -36,7 +35,7 @@ abstract class AbstractTemplateView extends \TYPO3Fluid\Fluid\View\AbstractTempl * ... * ) * - * @var array + * @var array */ protected $supportedOptions = [ 'templateRootPathPattern' => [ @@ -101,7 +100,7 @@ abstract class AbstractTemplateView extends \TYPO3Fluid\Fluid\View\AbstractTempl * * @see $supportedOptions * - * @var array + * @var array */ protected $options = []; @@ -124,6 +123,9 @@ public function assign($key, $value): self return parent::assign($key, $value); } + /** + * @param array $values + */ public function assignMultiple(array $values): self { // layer to fix incompatibility error with typo3 fluid interface @@ -133,7 +135,7 @@ public function assignMultiple(array $values): self /** * Factory method to create an instance with given options. * - * @param array $options + * @param array $options * @return static */ public static function createWithOptions(array $options): self @@ -144,7 +146,7 @@ public static function createWithOptions(array $options): self /** * Set default options based on the supportedOptions provided * - * @param array $options + * @param array|null $options * @throws Exception */ public function __construct(?array $options = null) @@ -184,14 +186,9 @@ public function setControllerContext(ControllerContext $controllerContext) $renderingContext->setControllerContext($controllerContext); } - $paths = $this->getTemplatePaths(); $request = $controllerContext->getRequest(); - if (!$request instanceof ActionRequest) { - return; - } - $paths->setFormat($request->getFormat()); if ($paths->getTemplateRootPaths() === [] && $paths->getLayoutRootPaths() === [] && $paths->getPartialRootPaths() === []) { @@ -205,7 +202,7 @@ public function setControllerContext(ControllerContext $controllerContext) * Renders a given section. * * @param string $sectionName Name of section to render - * @param array $variables The variables to use + * @param array $variables The variables to use * @param boolean $ignoreUnknown Ignore an unknown section and just return an empty string * @return string rendered template for the section * @throws \Neos\FluidAdaptor\View\Exception\InvalidSectionException @@ -217,13 +214,14 @@ public function renderSection($sectionName, array $variables = [], $ignoreUnknow $variables = $this->getRenderingContext()->getVariableProvider()->getAll(); } + /** @phpstan-ignore argument.type (we assume Fluid actually supports \ArrayAccess here, too) */ return parent::renderSection($sectionName, $variables, $ignoreUnknown); } /** * Validate options given to this view. * - * @param array $options + * @param array $options * @return void * @throws Exception */ @@ -250,10 +248,10 @@ function ($supportedOptionData, $supportedOptionName, $options) { * Merges the given options with the default values * and sets the resulting options in this object. * - * @param array $options + * @param array $options * @return void */ - protected function setOptions(array $options) + protected function setOptions(array $options): void { $this->options = array_merge( array_map( diff --git a/Neos.FluidAdaptor/Classes/View/StandaloneView.php b/Neos.FluidAdaptor/Classes/View/StandaloneView.php index 6fdb79413a..d4a3cbfc6f 100644 --- a/Neos.FluidAdaptor/Classes/View/StandaloneView.php +++ b/Neos.FluidAdaptor/Classes/View/StandaloneView.php @@ -51,7 +51,7 @@ class StandaloneView extends AbstractTemplateView protected $environment; /** - * @var ActionRequest + * @var ?ActionRequest */ protected $request; @@ -64,7 +64,7 @@ class StandaloneView extends AbstractTemplateView /** * Factory method to create an instance with given options. * - * @param array $options + * @param array $options * @return static */ public static function createWithOptions(array $options): self @@ -75,11 +75,12 @@ public static function createWithOptions(array $options): self /** * Constructor * - * @param ActionRequest $request The current action request. If none is specified it will be created from the environment. - * @param array $options + * @param ?ActionRequest $request The current action request. If none is specified it will be created from the environment. + * @param array|null $options * @throws \Neos\FluidAdaptor\Exception + * @phpstan-ignore method.childParameterType (parameters are differently ordered and thus cannot match) */ - public function __construct(?ActionRequest $request = null, array $options = []) + public function __construct(?ActionRequest $request = null, ?array $options = []) { $this->request = $request; parent::__construct($options); @@ -116,7 +117,7 @@ public function initializeObject() /** * @param string $templateName */ - public function setTemplate($templateName) + public function setTemplate($templateName): void { $this->baseRenderingContext->setControllerAction($templateName); } @@ -130,6 +131,9 @@ public function setTemplate($templateName) */ public function setFormat($format) { + if (!$this->request) { + throw new \Exception('Cannot set format without a request', 1743856310); + } $this->request->setFormat($format); $this->baseRenderingContext->getTemplatePaths()->setFormat($format); } @@ -137,18 +141,18 @@ public function setFormat($format) /** * Returns the format of the current request (defaults is "html") * - * @return string $format + * @return ?string $format * @api */ public function getFormat() { - return $this->request->getFormat(); + return $this->request?->getFormat(); } /** * Returns the current request object * - * @return ActionRequest + * @return ?ActionRequest */ public function getRequest() { @@ -177,12 +181,20 @@ public function setTemplatePathAndFilename($templatePathAndFilename) /** * Returns the absolute path to a Fluid template file if it was specified with setTemplatePathAndFilename() before * - * @return string Fluid template path + * @return ?string Fluid template path * @api */ public function getTemplatePathAndFilename() { - return $this->baseRenderingContext->getTemplatePaths()->resolveTemplateFileForControllerAndActionAndFormat($this->request->getControllerName(), $this->request->getControllerActionName(), $this->request->getFormat()); + return $this->request + ? $this->baseRenderingContext + ->getTemplatePaths() + ->resolveTemplateFileForControllerAndActionAndFormat( + $this->request->getControllerName(), + $this->request->getControllerActionName(), + $this->request->getFormat() + ) + : null; } /** @@ -225,7 +237,7 @@ public function setLayoutRootPaths(array $layoutRootPaths) /** * Resolves the layout root to be used inside other paths. * - * @return array Fluid layout root paths + * @return array Fluid layout root paths * @throws InvalidTemplateResourceException * @api */ @@ -262,7 +274,7 @@ public function setPartialRootPaths(array $partialRootPaths) /** * Returns the absolute path to the folder that contains Fluid partial files * - * @return array Fluid partial root paths + * @return array Fluid partial root paths * @throws InvalidTemplateResourceException * @api */ diff --git a/Neos.FluidAdaptor/Classes/View/TemplatePaths.php b/Neos.FluidAdaptor/Classes/View/TemplatePaths.php index ee45bae7eb..6c577db4ae 100644 --- a/Neos.FluidAdaptor/Classes/View/TemplatePaths.php +++ b/Neos.FluidAdaptor/Classes/View/TemplatePaths.php @@ -52,7 +52,7 @@ class TemplatePaths extends \TYPO3Fluid\Fluid\View\TemplatePaths ]; /** - * @var array + * @var array */ protected $options = []; @@ -61,6 +61,9 @@ class TemplatePaths extends \TYPO3Fluid\Fluid\View\TemplatePaths */ protected $packageManager; + /** + * @param array $options + */ public function __construct(array $options = []) { foreach ($options as $optionName => $optionValue) { @@ -71,7 +74,7 @@ public function __construct(array $options = []) /** * @param PackageManager $packageManager */ - public function injectPackageManager(PackageManager $packageManager) + public function injectPackageManager(PackageManager $packageManager): void { $this->packageManager = $packageManager; } @@ -87,7 +90,7 @@ public function getTemplateRootPathPattern(): string /** * @param string $templateRootPathPattern */ - public function setTemplateRootPathPattern(string $templateRootPathPattern) + public function setTemplateRootPathPattern(string $templateRootPathPattern): void { $this->templateRootPathPattern = $templateRootPathPattern; } @@ -95,7 +98,7 @@ public function setTemplateRootPathPattern(string $templateRootPathPattern) /** * @param string $layoutRootPathPattern */ - public function setLayoutRootPathPattern(string $layoutRootPathPattern) + public function setLayoutRootPathPattern(string $layoutRootPathPattern): void { $this->layoutRootPathPattern = $layoutRootPathPattern; } @@ -103,7 +106,7 @@ public function setLayoutRootPathPattern(string $layoutRootPathPattern) /** * @param string $partialRootPathPattern */ - public function setPartialRootPathPattern(string $partialRootPathPattern) + public function setPartialRootPathPattern(string $partialRootPathPattern): void { $this->partialRootPathPattern = $partialRootPathPattern; } @@ -111,7 +114,7 @@ public function setPartialRootPathPattern(string $partialRootPathPattern) /** * @param string $templateRootPath */ - public function setTemplateRootPath($templateRootPath) + public function setTemplateRootPath($templateRootPath): void { $this->templateRootPaths = [$templateRootPath]; } @@ -119,7 +122,7 @@ public function setTemplateRootPath($templateRootPath) /** * Resolves the template root to be used inside other paths. * - * @return array Path(s) to template root directory + * @return array Path(s) to template root directory */ public function getTemplateRootPaths() { @@ -140,7 +143,7 @@ public function getTemplateRootPaths() } /** - * @return array + * @return array */ public function getLayoutRootPaths() { @@ -160,7 +163,10 @@ public function getLayoutRootPaths() return [$layoutRootPath]; } - public function getPartialRootPaths() + /** + * @return array + */ + public function getPartialRootPaths(): array { if ($this->partialRootPaths !== []) { return $this->partialRootPaths; @@ -181,7 +187,7 @@ public function getPartialRootPaths() /** * @param string $layoutRootPath */ - public function setLayoutRootPath($layoutRootPath) + public function setLayoutRootPath($layoutRootPath): void { $this->layoutRootPaths = [$layoutRootPath]; } @@ -189,7 +195,7 @@ public function setLayoutRootPath($layoutRootPath) /** * @param string $partialRootPath */ - public function setPartialRootPath($partialRootPath) + public function setPartialRootPath($partialRootPath): void { $this->partialRootPaths = [$partialRootPath]; } @@ -205,7 +211,7 @@ public function getPatternReplacementVariables() /** * @param string[] $patternReplacementVariables */ - public function setPatternReplacementVariables($patternReplacementVariables) + public function setPatternReplacementVariables($patternReplacementVariables): void { $this->patternReplacementVariables = $patternReplacementVariables; } @@ -389,10 +395,10 @@ protected function sanitizePath($path) * replaced by the current request format, and once with ."@format" stripped off. * * @param string $pattern Pattern to be resolved - * @param array $patternReplacementVariables The variables to replace in the pattern + * @param array $patternReplacementVariables The variables to replace in the pattern * @param boolean $bubbleControllerAndSubpackage if true, then we successively split off parts from "@controller" and "@subpackage" until both are empty. * @param boolean $formatIsOptional if true, then half of the resulting strings will have ."@format" stripped off, and the other half will have it. - * @return array unix style paths + * @return array unix style paths */ protected function expandGenericPathPattern($pattern, array $patternReplacementVariables, $bubbleControllerAndSubpackage, $formatIsOptional) { @@ -426,10 +432,10 @@ protected function expandGenericPathPattern($pattern, array $patternReplacementV } /** - * @param array $paths + * @param array $paths * @param string $variableName * @param string $variableValue - * @return array + * @return array */ protected function replacePatternVariable($paths, $variableName, $variableValue) { @@ -441,11 +447,11 @@ protected function replacePatternVariable($paths, $variableName, $variableValue) } /** - * @param array $paths + * @param array $paths * @param string $controllerName * @param string $subPackageKey * @param bool $bubbleControllerAndSubpackage - * @return array + * @return array */ protected function expandSubPackageAndController(array $paths, string $controllerName, string $subPackageKey = '', bool $bubbleControllerAndSubpackage = false): array { @@ -460,7 +466,7 @@ protected function expandSubPackageAndController(array $paths, string $controlle $numberOfSubpackageParts = count($subpackageKeyParts); $subpackageReplacements = []; for ($i = 0; $i <= $numberOfSubpackageParts; $i++) { - $subpackageReplacements[] = implode('/', ($i < 0 ? $subpackageKeyParts : array_slice($subpackageKeyParts, $i))); + $subpackageReplacements[] = implode('/', array_slice($subpackageKeyParts, $i)); } $paths = $this->expandPatterns($paths, '@subpackage', $subpackageReplacements); @@ -476,10 +482,10 @@ protected function expandSubPackageAndController(array $paths, string $controlle * Expands the given $patterns by adding an array element for each $replacement * replacing occurrences of $search. * - * @param array $patterns + * @param array $patterns * @param string $search - * @param array $replacements - * @return array + * @param array $replacements + * @return array */ protected function expandPatterns(array $patterns, string $search, array $replacements): array { diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/DebugViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/DebugViewHelper.php index 0b189fa7b2..39d42bc727 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/DebugViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/DebugViewHelper.php @@ -78,6 +78,6 @@ public function render() $expressionToExamine = (is_object($expressionToExamine) ? get_class($expressionToExamine) : gettype($expressionToExamine)); } - return \Neos\Flow\var_dump($expressionToExamine, $this->arguments['title'], true); + return \Neos\Flow\var_dump($expressionToExamine, $this->arguments['title'], true) ?: ''; } } diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/FlashMessagesViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/FlashMessagesViewHelper.php index 5198e6ed28..73ccbe633e 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/FlashMessagesViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/FlashMessagesViewHelper.php @@ -105,7 +105,7 @@ public function render() * Render the flash messages as unsorted list. This is triggered if no "as" argument is given * to the ViewHelper. * - * @param list $flashMessages + * @param array $flashMessages * @return string */ protected function renderAsList(array $flashMessages) @@ -131,7 +131,7 @@ protected function renderAsList(array $flashMessages) * the flash messages are stored in the template inside the variable specified * in "as". * - * @param array $flashMessages + * @param array $flashMessages * @param string $as * @return string */ diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Form/AbstractFormFieldViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Form/AbstractFormFieldViewHelper.php index 489844b745..a3da57a863 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Form/AbstractFormFieldViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Form/AbstractFormFieldViewHelper.php @@ -84,7 +84,6 @@ protected function getNameWithoutPrefix(): string $name = $this->arguments['name']; } if ($this->hasArgument('value')) { - /** @var object $value */ $value = $this->arguments['value']; $multiple = $this->hasArgument('multiple') && $this->arguments['multiple'] === true; if (!$multiple @@ -223,7 +222,7 @@ protected function getPropertyPath(): string return $formObjectName . '.' . $this->arguments['property']; } - return rtrim(preg_replace('/(\]\[|\[|\])/', '.', $this->getNameWithoutPrefix()), '.'); + return rtrim(preg_replace('/(\]\[|\[|\])/', '.', $this->getNameWithoutPrefix()) ?: '', '.'); } /** diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Form/AbstractFormViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Form/AbstractFormViewHelper.php index 60aa6d6da6..c58c50daa5 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Form/AbstractFormViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Form/AbstractFormViewHelper.php @@ -41,7 +41,7 @@ public function injectPersistenceManager(PersistenceManagerInterface $persistenc /** * Prefixes / namespaces the given name with the form field prefix * - * @param string $fieldName field name to be prefixed + * @param ?string $fieldName field name to be prefixed * @return string namespaced field name */ protected function prefixFieldName($fieldName) @@ -68,13 +68,13 @@ protected function prefixFieldName($fieldName) * Renders a hidden form field containing the technical identity of the given object. * * @param object $object Object to create the identity field for - * @param string $name Name + * @param ?string $name Name * @return string A hidden field containing the Identity (UUID in Flow) of the given object or empty string if the object is unknown to the persistence framework * @see \Neos\Flow\Mvc\Controller\Argument::setValue() */ - protected function renderHiddenIdentityField($object, $name) + protected function renderHiddenIdentityField(object $object, $name) { - if (!is_object($object) || $this->persistenceManager->isNewObject($object)) { + if ($this->persistenceManager->isNewObject($object) || $name === null) { return ''; } $identifier = $this->persistenceManager->getIdentifierByObject($object); diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Form/CheckboxViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Form/CheckboxViewHelper.php index d7d4e3b574..67f85cf55b 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Form/CheckboxViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Form/CheckboxViewHelper.php @@ -98,7 +98,8 @@ public function render() if ($checked === null) { $checked = false; foreach ($propertyValue as $value) { - if (TypeHandling::isSimpleType(TypeHandling::getTypeForValue($value))) { + $typeForValue = TypeHandling::getTypeForValue($value); + if (TypeHandling::isSimpleType($typeForValue)) { $checked = $valueAttribute === $value; } else { // assume an entity diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Form/SelectViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Form/SelectViewHelper.php index a2dde44370..f9c0f08933 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Form/SelectViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Form/SelectViewHelper.php @@ -200,7 +200,7 @@ public function render() /** * Render the option tags. * - * @param array $options the options for the form. + * @param array $options the options for the form. * @return string rendered tags. */ protected function renderOptionTags($options) @@ -227,7 +227,7 @@ protected function renderOptionTags($options) /** * Render the option tags. * - * @return array an associative array of options, key will be the value of the option tag + * @return array an associative array of options, key will be the value of the option tag * @throws ViewHelper\Exception */ protected function getOptions() @@ -354,6 +354,7 @@ protected function getOptionValueScalar($valueElement) } elseif ($this->persistenceManager->getIdentifierByObject($valueElement) !== null) { return $this->persistenceManager->getIdentifierByObject($valueElement); } else { + /** @phpstan-ignore cast.string (we'll try anyway -.-) */ return (string)$valueElement; } } else { diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/FormViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/FormViewHelper.php index 9d627d6dcf..3500474fbb 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/FormViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/FormViewHelper.php @@ -299,7 +299,7 @@ protected function renderHiddenReferrerFields() $result = chr(10); $request = $this->controllerContext->getRequest(); $argumentNamespace = null; - if ($request instanceof ActionRequest && $request->isMainRequest() === false) { + if ($request->isMainRequest() === false) { $argumentNamespace = $request->getArgumentNamespace(); $referrer = [ @@ -313,13 +313,10 @@ protected function renderHiddenReferrerFields() $referrerValue = $referrerValue ? htmlspecialchars($referrerValue) : ''; $result .= '' . chr(10); } + /** @var ActionRequest $request we know this since $request is not the main request */ $request = $request->getParentRequest(); } - if ($request === null) { - throw new \RuntimeException('No ActionRequest could be found to evaluate form argument namespace.', 1565945918); - } - $arguments = $request->getArguments(); if ($argumentNamespace !== null && isset($arguments[$argumentNamespace])) { // A sub request was there; thus we can unset the sub requests arguments, diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/BytesViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/BytesViewHelper.php index 76a4b946e1..a538a7d88d 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/BytesViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/BytesViewHelper.php @@ -72,7 +72,7 @@ class BytesViewHelper extends AbstractLocaleAwareViewHelper /** * @param float $bytes - * @return array + * @return array{0: float, 1: string} */ protected static function maximizeUnit(float $bytes): array { diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/CaseViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/CaseViewHelper.php index 9dc3ecd2c2..8eefa16a80 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/CaseViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/CaseViewHelper.php @@ -123,7 +123,7 @@ public function render() } /** - * @param array $arguments + * @param array $arguments * @param \Closure $renderChildrenClosure * @param RenderingContextInterface $renderingContext * @return string diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/CropViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/CropViewHelper.php index 4ab2986948..e9de2cb5be 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/CropViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/CropViewHelper.php @@ -78,7 +78,7 @@ public function render() } /** - * @param array $arguments + * @param array $arguments * @param \Closure $renderChildrenClosure * @param RenderingContextInterface $renderingContext * @return string diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/HtmlentitiesDecodeViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/HtmlentitiesDecodeViewHelper.php index 0316b42dcc..660c7f2fee 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/HtmlentitiesDecodeViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/HtmlentitiesDecodeViewHelper.php @@ -79,7 +79,7 @@ public function render() /** * Applies html_entity_decode() on the specified value. * - * @param array $arguments + * @param array $arguments * @param \Closure $renderChildrenClosure * @param RenderingContextInterface $renderingContext * @return string|mixed diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/HtmlentitiesViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/HtmlentitiesViewHelper.php index a1d67c8532..0f0dbe84ed 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/HtmlentitiesViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/HtmlentitiesViewHelper.php @@ -79,7 +79,7 @@ public function render() /** * Applies htmlentities() on the specified value. * - * @param array $arguments + * @param array $arguments * @param \Closure $renderChildrenClosure * @param RenderingContextInterface $renderingContext * @return string|mixed diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/JsonViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/JsonViewHelper.php index 87c216d4d5..d8c7b228e4 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/JsonViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/JsonViewHelper.php @@ -86,7 +86,7 @@ public function render() $options = $options | JSON_FORCE_OBJECT; } - return json_encode($value, $options); + return json_encode($value, $options | JSON_THROW_ON_ERROR); } /** diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/PaddingViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/PaddingViewHelper.php index 476db7b55e..cc37d1f089 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/PaddingViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/PaddingViewHelper.php @@ -79,7 +79,7 @@ public function render() /** * Applies str_pad() on the specified value. * - * @param array $arguments + * @param array $arguments * @param \Closure $renderChildrenClosure * @param RenderingContextInterface $renderingContext * @return string diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/UrlencodeViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/UrlencodeViewHelper.php index f312b713b2..f0650790bb 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Format/UrlencodeViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Format/UrlencodeViewHelper.php @@ -72,7 +72,7 @@ public function render() /** * Applies rawurlencode() on the specified value. * - * @param array $arguments + * @param array $arguments * @param \Closure $renderChildrenClosure * @param RenderingContextInterface $renderingContext * @return string|mixed diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/RenderChildrenViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/RenderChildrenViewHelper.php index 9d35aca2ef..1041792388 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/RenderChildrenViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/RenderChildrenViewHelper.php @@ -74,7 +74,7 @@ public function render(): string $widgetChildNodes = $this->getWidgetChildNodes(); $this->addArgumentsToTemplateVariableContainer($this->arguments['arguments']); - $output = $widgetChildNodes->evaluate($renderingContext); + $output = $widgetChildNodes?->evaluate($renderingContext) ?: ''; $this->removeArgumentsFromTemplateVariableContainer($this->arguments['arguments']); return $output; @@ -97,10 +97,10 @@ protected function getWidgetRenderingContext(): RenderingContextInterface } /** - * @return RootNode + * @return ?RootNode * @throws WidgetContextNotFoundException */ - protected function getWidgetChildNodes(): RootNode + protected function getWidgetChildNodes(): ?RootNode { return $this->getWidgetContext()->getViewHelperChildNodes(); } @@ -123,7 +123,7 @@ protected function getWidgetContext(): WidgetContext /** * Add the given arguments to the TemplateVariableContainer of the widget. * - * @param array $arguments + * @param array $arguments * @return void * @throws RenderingContextNotFoundException * @throws WidgetContextNotFoundException @@ -139,7 +139,7 @@ protected function addArgumentsToTemplateVariableContainer(array $arguments): vo /** * Remove the given arguments from the TemplateVariableContainer of the widget. * - * @param array $arguments + * @param array $arguments * @return void * @throws RenderingContextNotFoundException * @throws WidgetContextNotFoundException diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Security/IfAccessViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Security/IfAccessViewHelper.php index 225621a2e7..e316a8243b 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Security/IfAccessViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Security/IfAccessViewHelper.php @@ -15,7 +15,6 @@ use Neos\Flow\Security\Authorization\PrivilegeManagerInterface; use Neos\Flow\Security\Context; use Neos\FluidAdaptor\Core\Rendering\FlowAwareRenderingContextInterface; -use Neos\FluidAdaptor\Core\Rendering\RenderingContext; use Neos\FluidAdaptor\Core\ViewHelper\AbstractConditionViewHelper; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; @@ -57,7 +56,7 @@ class IfAccessViewHelper extends AbstractConditionViewHelper /** * Initializes the "then" and "else" arguments */ - public function initializeArguments() + public function initializeArguments(): void { parent::initializeArguments(); $this->registerArgument('privilegeTarget', 'string', 'Condition expression conforming to Fluid boolean rules', true); @@ -80,9 +79,9 @@ public function render() } /** - * @param array $arguments + * @param array $arguments * @param \Closure $renderChildrenClosure - * @param RenderingContextInterface $renderingContext + * @param FlowAwareRenderingContextInterface&RenderingContextInterface $renderingContext * @return mixed */ public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext) @@ -91,28 +90,30 @@ public static function renderStatic(array $arguments, \Closure $renderChildrenCl } /** - * @param array|null $arguments + * @param array|null $arguments * @param FlowAwareRenderingContextInterface&RenderingContextInterface $renderingContext * @return boolean */ protected static function evaluateCondition($arguments, RenderingContextInterface $renderingContext) { $objectManager = $renderingContext->getObjectManager(); - /** @var Context $securityContext */ $securityContext = $objectManager->get(Context::class); - if ($securityContext !== null && !$securityContext->canBeInitialized()) { + if (!$securityContext->canBeInitialized()) { return false; } $privilegeManager = static::getPrivilegeManager($renderingContext); - return $privilegeManager->isPrivilegeTargetGranted($arguments['privilegeTarget'], $arguments['parameters'] ?? []); + $privilegeTarget = $arguments['privilegeTarget'] ?? null; + if (!is_string($privilegeTarget)) { + throw new \Exception('Missing privilegeTarget argument.', 1743848049); + } + return $privilegeManager->isPrivilegeTargetGranted($privilegeTarget, $arguments['parameters'] ?? []); } /** - * @param RenderingContext $renderingContext * @return PrivilegeManagerInterface */ - protected static function getPrivilegeManager(RenderingContext $renderingContext) + protected static function getPrivilegeManager(FlowAwareRenderingContextInterface $renderingContext) { $objectManager = $renderingContext->getObjectManager(); return $objectManager->get(PrivilegeManagerInterface::class); diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Security/IfAuthenticatedViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Security/IfAuthenticatedViewHelper.php index a7a78ae4b9..f7f6c6cb1f 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Security/IfAuthenticatedViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Security/IfAuthenticatedViewHelper.php @@ -66,7 +66,7 @@ public function render() } /** - * @param null $arguments + * @param array|null $arguments * @param FlowAwareRenderingContextInterface&RenderingContextInterface $renderingContext * @return bool */ diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Security/IfHasRoleViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Security/IfHasRoleViewHelper.php index 9be411ea51..5db4eeac33 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Security/IfHasRoleViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Security/IfHasRoleViewHelper.php @@ -79,7 +79,7 @@ class IfHasRoleViewHelper extends AbstractConditionViewHelper /** * Initializes the "then" and "else" arguments */ - public function initializeArguments() + public function initializeArguments(): void { parent::initializeArguments(); $this->registerArgument('role', 'mixed', 'The role or role identifier.', true); @@ -104,7 +104,7 @@ public function render() } /** - * @param array|null $arguments + * @param array|null $arguments * @param FlowAwareRenderingContextInterface&RenderingContextInterface $renderingContext * @return boolean */ @@ -120,8 +120,8 @@ protected static function evaluateCondition($arguments, RenderingContextInterfac return false; } - $role = $arguments['role']; - $account = $arguments['account']; + $role = $arguments['role'] ?? null; + $account = $arguments['account'] ?? null; $packageKey = isset($arguments['packageKey']) ? $arguments['packageKey'] : $renderingContext->getControllerContext()->getRequest()->getControllerPackageKey(); if (is_string($role)) { diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/TranslateViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/TranslateViewHelper.php index c4c0d06f0b..8db5694bc1 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/TranslateViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/TranslateViewHelper.php @@ -15,7 +15,6 @@ use Neos\Flow\I18n\Exception\InvalidLocaleIdentifierException; use Neos\Flow\I18n\Locale; use Neos\Flow\I18n\Translator; -use Neos\Flow\Mvc\ActionRequest; use Neos\FluidAdaptor\Core\ViewHelper; use Neos\FluidAdaptor\Core\ViewHelper\Exception as ViewHelperException; @@ -122,9 +121,7 @@ public function render() } if ($package === null) { $request = $this->renderingContext->getControllerContext()->getRequest(); - if ($request instanceof ActionRequest) { - $package = $request->getControllerPackageKey(); - } + $package = $request->getControllerPackageKey(); if (empty($package)) { throw new ViewHelperException( 'The current package key can\'t be resolved. Make sure to initialize the Fluid view with a proper ActionRequest and/or specify the "package" argument when using the f:translate ViewHelper', @@ -151,7 +148,7 @@ public function render() /** * @param Translator $translator */ - public function injectTranslator(Translator $translator) + public function injectTranslator(Translator $translator): void { $this->translator = $translator; } diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Uri/ResourceViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Uri/ResourceViewHelper.php index 6254572c13..9eebbb0346 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Uri/ResourceViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Uri/ResourceViewHelper.php @@ -100,7 +100,7 @@ public function render() } /** - * @param array $arguments + * @param array $arguments * @param \Closure $renderChildrenClosure * @param FlowAwareRenderingContextInterface&RenderingContextInterface $renderingContext * @return string @@ -145,7 +145,7 @@ public static function renderStatic(array $arguments, \Closure $renderChildrenCl $resourcePath = 'resource://' . $package . '/Public/' . $path; $localizedResourcePathData = $i18nService->getLocalizedFilename($resourcePath); $matches = []; - if (preg_match('#resource://([^/]+)/Public/(.*)#', current($localizedResourcePathData), $matches) === 1) { + if (preg_match('#resource://([^/]+)/Public/(.*)#', (string)current($localizedResourcePathData), $matches) === 1) { $package = $matches[1]; $path = $matches[2]; } diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Validation/IfHasErrorsViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Validation/IfHasErrorsViewHelper.php index 3efab8366c..41f1a01f6a 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Validation/IfHasErrorsViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Validation/IfHasErrorsViewHelper.php @@ -41,11 +41,7 @@ */ class IfHasErrorsViewHelper extends AbstractConditionViewHelper { - /** - * @return void - * @throws \Neos\FluidAdaptor\Core\ViewHelper\Exception - */ - public function initializeArguments() + public function initializeArguments(): void { $this->registerArgument('then', 'mixed', 'Value to be returned if the condition if met.', false); $this->registerArgument('else', 'mixed', 'Value to be returned if the condition if not met.', false); @@ -70,7 +66,7 @@ public function render() } /** - * @param null $arguments + * @param array $arguments * @param FlowAwareRenderingContextInterface&RenderingContextInterface $renderingContext * @return boolean */ diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/Controller/AutocompleteController.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/Controller/AutocompleteController.php index 9dd40909a6..a9468b212a 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/Controller/AutocompleteController.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/Controller/AutocompleteController.php @@ -21,7 +21,7 @@ class AutocompleteController extends AbstractWidgetController { /** - * @var array + * @var array */ protected $configuration = ['limit' => 10]; @@ -82,6 +82,6 @@ public function autocompleteAction($term) 'value' => $val ]; } - return json_encode($output); + return json_encode($output, JSON_THROW_ON_ERROR); } } diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/Controller/PaginateController.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/Controller/PaginateController.php index 1778c2c0a1..a7b50ed016 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/Controller/PaginateController.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/Controller/PaginateController.php @@ -26,7 +26,7 @@ class PaginateController extends AbstractWidgetController protected $objects; /** - * @var array + * @var array */ protected $configuration = ['itemsPerPage' => 10, 'insertAbove' => false, 'insertBelow' => true, 'maximumNumberOfLinks' => 99]; @@ -130,16 +130,24 @@ protected function calculateDisplayRange() } /** - * Returns an array with the keys "pages", "current", "numberOfPages", "nextPage" & "previousPage" - * - * @return array + * @return array{ + * pages: list, + * current: int, + * numberOfPages: int, + * displayRangeStart: float, + * displayRangeEnd: float, + * hasLessPages: bool, + * hasMorePages: bool, + * nextPage?: int, + * previousPage?: int, + * } */ protected function buildPagination() { $this->calculateDisplayRange(); $pages = []; for ($i = $this->displayRangeStart; $i <= $this->displayRangeEnd; $i++) { - $pages[] = ['number' => $i, 'isCurrent' => ($i === $this->currentPage)]; + $pages[] = ['number' => (int)$i, 'isCurrent' => ($i == $this->currentPage)]; } $pagination = [ 'pages' => $pages, diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/LinkViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/LinkViewHelper.php index 55de2b8c53..5a6800624a 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/LinkViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/LinkViewHelper.php @@ -120,7 +120,7 @@ protected function getAjaxUri(): string } else { $arguments['__widgetId'] = $widgetContext->getAjaxWidgetIdentifier(); } - return '?' . http_build_query($arguments, null, '&'); + return '?' . http_build_query(data: $arguments, arg_separator: '&'); } /** diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/UriViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/UriViewHelper.php index f96c6702e2..a57cb01bcf 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/UriViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Widget/UriViewHelper.php @@ -104,7 +104,7 @@ protected function getAjaxUri(): string } else { $arguments['__widgetId'] = $widgetContext->getAjaxWidgetIdentifier(); } - return '?' . http_build_query($arguments, null, '&'); + return '?' . http_build_query(data: $arguments, arg_separator: '&'); } /** diff --git a/Neos.Http.Factories/Classes/FlowUploadedFile.php b/Neos.Http.Factories/Classes/FlowUploadedFile.php index e9f5e296cb..be9592c68b 100644 --- a/Neos.Http.Factories/Classes/FlowUploadedFile.php +++ b/Neos.Http.Factories/Classes/FlowUploadedFile.php @@ -14,12 +14,12 @@ class FlowUploadedFile extends UploadedFile * This is either the persistent identifier of a previously submitted resource file * or an array with the "__identity" key set to the persistent identifier. * - * @var array|string + * @var array{__identity: string}|string|null */ protected $originallySubmittedResource; /** - * @var string + * @var ?string */ protected $collectionName; @@ -27,7 +27,7 @@ class FlowUploadedFile extends UploadedFile * This is either the persistent identifier of a previously submitted resource file * or an array with the "__identity" key set to the persistent identifier. * - * @return array|string + * @return array{__identity: string}|string|null */ public function getOriginallySubmittedResource() { @@ -40,15 +40,15 @@ public function getOriginallySubmittedResource() * This is either the persistent identifier of a previously submitted resource file * or an array with the "__identity" key set to the persistent identifier. * - * @param array|string $originallySubmittedResource + * @param array{__identity: string}|string $originallySubmittedResource */ - public function setOriginallySubmittedResource($originallySubmittedResource) + public function setOriginallySubmittedResource($originallySubmittedResource): void { $this->originallySubmittedResource = $originallySubmittedResource; } /** - * @return string + * @return ?string */ public function getCollectionName() { @@ -58,7 +58,7 @@ public function getCollectionName() /** * @param string $collectionName */ - public function setCollectionName($collectionName) + public function setCollectionName($collectionName): void { $this->collectionName = $collectionName; } diff --git a/Neos.Http.Factories/Classes/RequestFactoryTrait.php b/Neos.Http.Factories/Classes/RequestFactoryTrait.php index 364bfb21de..b840389da0 100644 --- a/Neos.Http.Factories/Classes/RequestFactoryTrait.php +++ b/Neos.Http.Factories/Classes/RequestFactoryTrait.php @@ -13,6 +13,7 @@ trait RequestFactoryTrait { /** + * @param non-empty-string $method * @inheritDoc */ public function createRequest(string $method, $uri): RequestInterface diff --git a/Neos.Http.Factories/Classes/ServerRequestFactoryTrait.php b/Neos.Http.Factories/Classes/ServerRequestFactoryTrait.php index b4be972e24..58b0e52ac1 100644 --- a/Neos.Http.Factories/Classes/ServerRequestFactoryTrait.php +++ b/Neos.Http.Factories/Classes/ServerRequestFactoryTrait.php @@ -56,6 +56,7 @@ public function __construct( /** * @inheritDoc + * @param array $serverParams */ public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface { diff --git a/Neos.Http.Factories/Classes/StreamFactoryTrait.php b/Neos.Http.Factories/Classes/StreamFactoryTrait.php index 6c79adcc84..f8201880bc 100644 --- a/Neos.Http.Factories/Classes/StreamFactoryTrait.php +++ b/Neos.Http.Factories/Classes/StreamFactoryTrait.php @@ -18,6 +18,9 @@ trait StreamFactoryTrait public function createStream(string $content = ''): StreamInterface { $fileHandle = fopen('php://temp', 'r+'); + if (!is_resource($fileHandle)) { + throw new \Exception('unable to open php://temp', 1743846274); + } fwrite($fileHandle, $content); rewind($fileHandle); @@ -30,11 +33,15 @@ public function createStream(string $content = ''): StreamInterface public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface { $fileHandle = fopen($filename, $mode); + if (!is_resource($fileHandle)) { + throw new \Exception('unable to open ' . $filename, 1743846243); + } return $this->createStreamFromResource($fileHandle); } /** * @inheritDoc + * @param resource $resource */ public function createStreamFromResource($resource): StreamInterface { diff --git a/Neos.Kickstarter/Classes/Command/KickstartCommandController.php b/Neos.Kickstarter/Classes/Command/KickstartCommandController.php index 246b927c0c..ef9dd18a25 100644 --- a/Neos.Kickstarter/Classes/Command/KickstartCommandController.php +++ b/Neos.Kickstarter/Classes/Command/KickstartCommandController.php @@ -49,7 +49,7 @@ class KickstartCommandController extends CommandController * @param string $packageType Optional package type, e.g. "neos-plugin" * @see neos.flow:package:create */ - public function packageCommand($packageKey, $packageType = PackageInterface::DEFAULT_COMPOSER_TYPE) + public function packageCommand($packageKey, $packageType = PackageInterface::DEFAULT_COMPOSER_TYPE): void { $this->validatePackageKey($packageKey); @@ -101,7 +101,7 @@ public function packageCommand($packageKey, $packageType = PackageInterface::DEF * @param boolean $force Overwrite any existing controller or template code. Regardless of this flag, the package, model and repository will never be overwritten. * @see neos.kickstarter:kickstart:commandcontroller */ - public function actionControllerCommand($packageKey, $controllerName, $generateActions = false, $generateTemplates = true, $generateFusion = false, $generateRelated = false, $force = false) + public function actionControllerCommand($packageKey, $controllerName, $generateActions = false, $generateTemplates = true, $generateFusion = false, $generateRelated = false, $force = false): void { $subpackageName = ''; if (strpos($packageKey, '/') !== false) { @@ -195,7 +195,7 @@ public function actionControllerCommand($packageKey, $controllerName, $generateA * @param boolean $force Overwrite any existing controller. * @see neos.kickstarter:kickstart:actioncontroller */ - public function commandControllerCommand($packageKey, $controllerName, $force = false) + public function commandControllerCommand($packageKey, $controllerName, $force = false): void { $this->validatePackageKey($packageKey); if (!$this->packageManager->isPackageAvailable($packageKey)) { @@ -222,7 +222,7 @@ public function commandControllerCommand($packageKey, $controllerName, $force = * @param boolean $force Overwrite any existing model. * @see neos.kickstarter:kickstart:repository */ - public function modelCommand($packageKey, $modelName, $force = false) + public function modelCommand($packageKey, $modelName, $force = false): void { $this->validatePackageKey($packageKey); if (!$this->packageManager->isPackageAvailable($packageKey)) { @@ -264,7 +264,7 @@ public function modelCommand($packageKey, $modelName, $force = false) * @param boolean $force Overwrite any existing repository. * @see neos.kickstarter:kickstart:model */ - public function repositoryCommand($packageKey, $modelName, $force = false) + public function repositoryCommand($packageKey, $modelName, $force = false): void { $this->validatePackageKey($packageKey); if (!$this->packageManager->isPackageAvailable($packageKey)) { @@ -283,7 +283,7 @@ public function repositoryCommand($packageKey, $modelName, $force = false) * * @param string $packageKey The package key of the package for the documentation */ - public function documentationCommand($packageKey) + public function documentationCommand($packageKey): void { $this->validatePackageKey($packageKey); if (!$this->packageManager->isPackageAvailable($packageKey)) { @@ -303,7 +303,7 @@ public function documentationCommand($packageKey) * * @param string $packageKey The package key of the package for the translation * @param string $sourceLanguageKey The language key of the default language - * @param array $targetLanguageKeys Comma separated language keys for the target translations + * @param array $targetLanguageKeys Comma separated language keys for the target translations * @return void */ public function translationCommand($packageKey, $sourceLanguageKey, array $targetLanguageKeys = []) @@ -339,7 +339,7 @@ protected function validatePackageKey($packageKey) * @param string $modelName * @see http://www.php.net/manual/en/reserved.keywords.php */ - protected function validateModelName($modelName) + protected function validateModelName($modelName): void { if (Validation::isReservedKeyword($modelName)) { $this->outputLine('The name of the model cannot be one of the reserved words of PHP!'); diff --git a/Neos.Kickstarter/Classes/Service/GeneratorService.php b/Neos.Kickstarter/Classes/Service/GeneratorService.php index e872b5f181..7ff7b55ba2 100644 --- a/Neos.Kickstarter/Classes/Service/GeneratorService.php +++ b/Neos.Kickstarter/Classes/Service/GeneratorService.php @@ -51,9 +51,9 @@ class GeneratorService protected $reflectionService; /** - * @var array + * @var array */ - protected $generatedFiles = []; + protected array $generatedFiles = []; /** * Generate a controller with the given name for the given package @@ -63,7 +63,7 @@ class GeneratorService * @param string $controllerName The name of the new controller * @param boolean $fusionView If the controller should default to a FusionView * @param boolean $overwrite Overwrite any existing files? - * @return array An array of generated filenames + * @return array An array of generated filenames */ public function generateActionController($packageKey, $subpackage, $controllerName, $fusionView = false, $overwrite = false) { @@ -102,7 +102,7 @@ public function generateActionController($packageKey, $subpackage, $controllerNa * @param string $controllerName The name of the new controller * @param boolean $fusionView If the controller should default to a FusionView * @param boolean $overwrite Overwrite any existing files? - * @return array An array of generated filenames + * @return array An array of generated filenames */ public function generateCrudController($packageKey, $subpackage, $controllerName, $fusionView = false, $overwrite = false) { @@ -143,7 +143,7 @@ public function generateCrudController($packageKey, $subpackage, $controllerName * @param string $packageKey The package key of the controller's package * @param string $controllerName The name of the new controller * @param boolean $overwrite Overwrite any existing files? - * @return array An array of generated filenames + * @return array An array of generated filenames */ public function generateCommandController($packageKey, $controllerName, $overwrite = false) { @@ -179,7 +179,7 @@ public function generateCommandController($packageKey, $controllerName, $overwri * @param string $viewName The name of the view * @param string $templateName The name of the view * @param boolean $overwrite Overwrite any existing files? - * @return array An array of generated filenames + * @return array An array of generated filenames */ public function generateView($packageKey, $subpackage, $controllerName, $viewName, $templateName, $overwrite = false) { @@ -199,7 +199,9 @@ public function generateView($packageKey, $subpackage, $controllerName, $viewNam $contextVariables['modelFullClassName'] = '\\' . trim($baseNamespace, '\\') . ($subpackage != '' ? '\\' . $subpackage : '') . '\Domain\Model\\' . $controllerName; $contextVariables['modelClassName'] = ucfirst($contextVariables['modelName']); - $modelClassSchema = $this->reflectionService->getClassSchema($contextVariables['modelFullClassName']); + $modelClassSchema = class_exists($contextVariables['modelFullClassName']) + ? $this->reflectionService->getClassSchema($contextVariables['modelFullClassName']) + : null; if ($modelClassSchema !== null) { $contextVariables['properties'] = $modelClassSchema->getProperties(); if (isset($contextVariables['properties']['Persistence_Object_Identifier'])) { @@ -229,7 +231,7 @@ public function generateView($packageKey, $subpackage, $controllerName, $viewNam * @param string $packageKey The package key of the controller's package * @param string $layoutName The name of the layout * @param boolean $overwrite Overwrite any existing files? - * @return array An array of generated filenames + * @return array An array of generated filenames */ public function generateLayout($packageKey, $layoutName, $overwrite = false) { @@ -261,7 +263,7 @@ public function generateLayout($packageKey, $layoutName, $overwrite = false) * @param string $viewName The name of the view * @param string $templateName The name of the view * @param boolean $overwrite Overwrite any existing files? - * @return array An array of generated filenames + * @return array An array of generated filenames */ public function generateFusion(string $packageKey, string $subpackage, string $controllerName, string $viewName, string $templateName, bool $overwrite = false): array { @@ -281,7 +283,9 @@ public function generateFusion(string $packageKey, string $subpackage, string $c $contextVariables['modelFullClassName'] = '\\' . trim($baseNamespace, '\\') . ($subpackage != '' ? '\\' . $subpackage : '') . '\Domain\Model\\' . $controllerName; $contextVariables['modelClassName'] = ucfirst($contextVariables['modelName']); - $modelClassSchema = $this->reflectionService->getClassSchema($contextVariables['modelFullClassName']); + $modelClassSchema = class_exists($contextVariables['modelFullClassName']) + ? $this->reflectionService->getClassSchema($contextVariables['modelFullClassName']) + : null; if ($modelClassSchema !== null) { $contextVariables['properties'] = $modelClassSchema->getProperties(); if (isset($contextVariables['properties']['Persistence_Object_Identifier'])) { @@ -311,7 +315,7 @@ public function generateFusion(string $packageKey, string $subpackage, string $c * @param string $packageKey The package key of the controller's package * @param string $layoutName The name of the layout * @param boolean $overwrite Overwrite any existing files? - * @return array An array of generated filenames + * @return array An array of generated filenames */ public function generatePrototype(string $packageKey, string $layoutName, bool $overwrite = false): array { @@ -340,9 +344,9 @@ public function generatePrototype(string $packageKey, string $layoutName, bool $ * * @param string $packageKey The package key of the controller's package * @param string $modelName The name of the new model - * @param array $fieldDefinitions The field definitions + * @param array $fieldDefinitions The field definitions * @param boolean $overwrite Overwrite any existing files? - * @return array An array of generated filenames + * @return array An array of generated filenames */ public function generateModel($packageKey, $modelName, array $fieldDefinitions, $overwrite = false) { @@ -378,7 +382,7 @@ public function generateModel($packageKey, $modelName, array $fieldDefinitions, * @param string $packageKey The package key of the controller's package * @param string $modelName The name of the new model fpr which to generate the test * @param boolean $overwrite Overwrite any existing files? - * @return array An array of generated filenames + * @return array An array of generated filenames */ public function generateTestsForModel($packageKey, $modelName, $overwrite = false) { @@ -411,7 +415,7 @@ public function generateTestsForModel($packageKey, $modelName, $overwrite = fals * @param string $packageKey The package key * @param string $modelName The name of the model * @param boolean $overwrite Overwrite any existing files? - * @return array An array of generated filenames + * @return array An array of generated filenames */ public function generateRepository($packageKey, $modelName, $overwrite = false) { @@ -443,7 +447,7 @@ public function generateRepository($packageKey, $modelName, $overwrite = false) * Generate a documentation skeleton for the package key * * @param string $packageKey The package key - * @return array An array of generated filenames + * @return array An array of generated filenames */ public function generateDocumentation($packageKey) { @@ -459,7 +463,7 @@ public function generateDocumentation($packageKey) $templatePathAndFilename = 'resource://Neos.Kickstarter/Private/Generator/Documentation/Makefile'; $fileContent = file_get_contents($templatePathAndFilename); $targetPathAndFilename = $documentationPath . '/Makefile'; - $this->generateFile($targetPathAndFilename, $fileContent); + $this->generateFile($targetPathAndFilename, $fileContent ?: ''); $templatePathAndFilename = 'resource://Neos.Kickstarter/Private/Generator/Documentation/index.rst'; $fileContent = $this->renderTemplate($templatePathAndFilename, $contextVariables); @@ -481,8 +485,8 @@ public function generateDocumentation($packageKey) * * @param string $packageKey * @param string $sourceLanguageKey - * @param array $targetLanguageKeys - * @return array An array of generated filenames + * @param array $targetLanguageKeys + * @return array An array of generated filenames */ public function generateTranslation($packageKey, $sourceLanguageKey, array $targetLanguageKeys = []) { @@ -516,9 +520,9 @@ public function generateTranslation($packageKey, $sourceLanguageKey, array $targ /** * Normalize types and prefix types with namespaces * - * @param array $fieldDefinitions The field definitions + * @param array $fieldDefinitions The field definitions * @param string $namespace The namespace - * @return array The normalized and type converted field definitions + * @return array The normalized and type converted field definitions */ protected function normalizeFieldDefinitions(array $fieldDefinitions, $namespace = '') { @@ -574,7 +578,7 @@ protected function generateFile($targetPathAndFilename, $fileContent, $force = f * Render the given template file with the given variables * * @param string $templatePathAndFilename - * @param array $contextVariables + * @param array $contextVariables * @return string * @throws \Neos\FluidAdaptor\Core\Exception */ @@ -588,7 +592,7 @@ protected function renderTemplate($templatePathAndFilename, array $contextVariab /** * @param PackageInterface $package - * @return array + * @return array */ protected function getPrimaryNamespaceAndEntryPath(PackageInterface $package) { diff --git a/Neos.Kickstarter/Classes/Utility/Inflector.php b/Neos.Kickstarter/Classes/Utility/Inflector.php index ed402234f7..49fa0eb222 100644 --- a/Neos.Kickstarter/Classes/Utility/Inflector.php +++ b/Neos.Kickstarter/Classes/Utility/Inflector.php @@ -56,6 +56,6 @@ public function humanizeCamelCase($camelCased, $lowercase = false) */ protected function spacify($camelCased, $glue = ' ') { - return preg_replace('/([a-z0-9])([A-Z])/', '$1' . $glue . '$2', $camelCased); + return preg_replace('/([a-z0-9])([A-Z])/', '$1' . $glue . '$2', $camelCased) ?: ''; } } diff --git a/Neos.Utility.Arrays/Classes/Arrays.php b/Neos.Utility.Arrays/Classes/Arrays.php index 71a44f2aeb..f618721d75 100644 --- a/Neos.Utility.Arrays/Classes/Arrays.php +++ b/Neos.Utility.Arrays/Classes/Arrays.php @@ -22,9 +22,9 @@ abstract class Arrays * Explodes a $string delimited by $delimiter and passes each item in the array through intval(). * Corresponds to explode(), but with conversion to integers for all values. * - * @param string $delimiter Delimiter string to explode with + * @param non-empty-string $delimiter Delimiter string to explode with * @param string $string The string to explode - * @return array Exploded values, all converted to integers + * @return array Exploded values, all converted to integers */ public static function integerExplode(string $delimiter, string $string): array { @@ -41,10 +41,10 @@ public static function integerExplode(string $delimiter, string $string): array * Explodes a string and trims all values for whitespace in the ends. * If $onlyNonEmptyValues is set, then all blank ('') values are removed. * - * @param string $delimiter Delimiter string to explode with + * @param non-empty-string $delimiter Delimiter string to explode with * @param string $string The string to explode * @param boolean $onlyNonEmptyValues If disabled, even empty values (='') will be set in output - * @return array Exploded values + * @return array Exploded values */ public static function trimExplode(string $delimiter, string $string, bool $onlyNonEmptyValues = true): array { @@ -64,11 +64,11 @@ public static function trimExplode(string $delimiter, string $string, bool $only * in the first array ($firstArray) with the values of the second array ($secondArray) in case of identical keys, * ie. keeping the values of the second. * - * @param array $firstArray First array - * @param array $secondArray Second array, overruling the first array + * @param array $firstArray First array + * @param array $secondArray Second array, overruling the first array * @param boolean $dontAddNewKeys If set, keys that are NOT found in $firstArray (first array) will not be set. Thus only existing value can/will be overruled from second array. * @param boolean $emptyValuesOverride If set (which is the default), values from $secondArray will overrule if they are empty (according to PHP's empty() function) - * @return array Resulting array where $secondArray values has overruled $firstArray values + * @return array Resulting array where $secondArray values has overruled $firstArray values */ public static function arrayMergeRecursiveOverrule(array $firstArray, array $secondArray, bool $dontAddNewKeys = false, bool $emptyValuesOverride = true): array { @@ -109,13 +109,13 @@ public static function arrayMergeRecursiveOverrule(array $firstArray, array $sec * Merges two arrays recursively and "binary safe" (integer keys are overridden as well), overruling similar values in the first array ($firstArray) with the values of the second array ($secondArray) * In case of identical keys, ie. keeping the values of the second. The given $toArray closure will be used if one of the two array keys contains an array and the other not. It should return an array. * - * @param array $firstArray First array - * @param array $secondArray Second array, overruling the first array + * @param array $firstArray First array + * @param array $secondArray Second array, overruling the first array * @param \Closure $toArray The given callable will get a value that is not an array and has to return an array. * This is to allow custom merging of simple types with (sub) arrays * @param \Closure|null $overrideFirst The given callable will determine whether the value of the first array should be overridden. * It should have the following signature $callable($key, ?array $firstValue = null, ?array $secondValue = null): bool - * @return array Resulting array where $secondArray values has overruled $firstArray values + * @return array Resulting array where $secondArray values has overruled $firstArray values */ public static function arrayMergeRecursiveOverruleWithCallback(array $firstArray, array $secondArray, \Closure $toArray, ?\Closure $overrideFirst = null): array { @@ -157,8 +157,7 @@ public static function arrayMergeRecursiveOverruleWithCallback(array $firstArray /** * Returns true if the given array contains elements of varying types * - * @param array $array - * @return boolean + * @param array $array */ public static function containsMultipleTypes(array $array): bool { @@ -179,12 +178,12 @@ public static function containsMultipleTypes(array $array): bool * Replacement for array_reduce that allows any type for $initial (instead * of only integer) * - * @param array $array the array to reduce - * @param string $function the reduce function with the same order of parameters as in the native array_reduce (i.e. accumulator first, then current array element) + * @param array $array the array to reduce + * @param callable $function the reduce function with the same order of parameters as in the native array_reduce (i.e. accumulator first, then current array element) * @param mixed $initial the initial accumulator value * @return mixed */ - public static function array_reduce(array $array, string $function, $initial = null) + public static function array_reduce(array $array, callable $function, $initial = null) { $accumulator = $initial; foreach ($array as $value) { @@ -196,8 +195,8 @@ public static function array_reduce(array $array, string $function, $initial = n /** * Returns the value of a nested array by following the specifed path. * - * @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' + * @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' * @return mixed The value found, NULL if the path didn't exist (note there is no way to distinguish between a found NULL value and "path not found") */ public static function getValueByPath(array $array, array|string $path): mixed @@ -222,8 +221,8 @@ public static function getValueByPath(array $array, array|string $path): mixed * See {@see ValueAccessor} * * @internal experimental feature, not stable API - * @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' + * @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' * @return ValueAccessor */ public static function getAccessorByPath(array $array, array|string $path): ValueAccessor @@ -235,21 +234,17 @@ public static function getAccessorByPath(array $array, array|string $path): Valu /** * Sets the given value in a nested array or object by following the specified path. * - * @param array|\ArrayAccess $subject The array or ArrayAccess instance to work on - * @param array|string $path The path to follow. Either a simple array of keys or a string in the format 'foo.bar.baz' + * @param array|\ArrayAccess $subject The array or ArrayAccess instance to work on + * @param array|string $path The path to follow. Either a simple array of keys or a string in the format 'foo.bar.baz' * @param mixed $value The value to set - * @return array|\ArrayAccess The modified array or object + * @phpstan-return ($subject is array ? array : \ArrayAccess) + * @return array|\ArrayAccess The modified array or object * @throws \InvalidArgumentException */ - public static function setValueByPath($subject, $path, $value) + public static function setValueByPath(array|\ArrayAccess $subject, array|string $path, $value) { - if (!is_array($subject) && !($subject instanceof \ArrayAccess)) { - throw new \InvalidArgumentException('setValueByPath() expects $subject to be array or an object implementing \ArrayAccess, "' . (is_object($subject) ? get_class($subject) : gettype($subject)) . '" given.', 1306424308); - } if (is_string($path)) { $path = explode('.', $path); - } elseif (!is_array($path)) { - throw new \InvalidArgumentException('setValueByPath() expects $path to be string or array, "' . gettype($path) . '" given.', 1305111499); } $key = array_shift($path); if (count($path) === 0) { @@ -266,17 +261,15 @@ public static function setValueByPath($subject, $path, $value) /** * Unsets an element/part of a nested array by following the specified path. * - * @param array $array The array - * @param array|string $path The path to follow. Either a simple array of keys or a string in the format 'foo.bar.baz' - * @return array The modified array + * @param array $array The array + * @param array|string $path The path to follow. Either a simple array of keys or a string in the format 'foo.bar.baz' + * @return array The modified array * @throws \InvalidArgumentException */ - public static function unsetValueByPath(array $array, $path): array + public static function unsetValueByPath(array $array, array|string $path): array { if (is_string($path)) { $path = explode('.', $path); - } elseif (!is_array($path)) { - throw new \InvalidArgumentException('unsetValueByPath() expects $path to be string or array, "' . gettype($path) . '" given.', 1305111513); } $key = array_shift($path); if (count($path) === 0) { @@ -293,7 +286,7 @@ public static function unsetValueByPath(array $array, $path): array /** * Sorts multidimensional arrays by recursively calling ksort on its elements. * - * @param array $array the array to sort + * @param array $array the array to sort * @param integer $sortFlags may be used to modify the sorting behavior using these values (see http://www.php.net/manual/en/function.sort.php) * @return boolean true on success, false on failure * @see asort() @@ -314,7 +307,7 @@ public static function sortKeysRecursively(array &$array, int $sortFlags = \SORT * Recursively convert an object hierarchy into an associative array. * * @param mixed $subject An object or array of objects - * @return array The subject represented as an array + * @return array The subject represented as an array * @throws \InvalidArgumentException */ public static function convertObjectToArray($subject): array @@ -336,8 +329,8 @@ public static function convertObjectToArray($subject): array /** * Recursively removes empty array elements. * - * @param array $array - * @return array the modified array + * @param array $array + * @return array the modified array */ public static function removeEmptyElementsRecursively(array $array): array { diff --git a/Neos.Utility.Arrays/Classes/PositionalArraySorter.php b/Neos.Utility.Arrays/Classes/PositionalArraySorter.php index e900ddd58d..fd05be60ed 100644 --- a/Neos.Utility.Arrays/Classes/PositionalArraySorter.php +++ b/Neos.Utility.Arrays/Classes/PositionalArraySorter.php @@ -44,18 +44,33 @@ */ final class PositionalArraySorter { + /** + * @var array + */ private array $startKeys; + /** + * @var array + */ private array $middleKeys; + /** + * @var array + */ private array $endKeys; + /** + * @var array + */ private array $beforeKeys; + /** + * @var array + */ private array $afterKeys; /** - * @param array $subject The source array to sort + * @param array $subject The source array to sort * @param string $positionPropertyPath optional property path to the string that contains the position * @param bool $removeNullValues if set to TRUE (default), null-values of the subject are removed */ @@ -69,7 +84,7 @@ public function __construct( /** * Returns a sorted copy of the subject array * - * @return array + * @return array * @throws Exception\InvalidPositionException */ public function toArray(): array @@ -87,7 +102,7 @@ public function toArray(): array * * TODO Detect circles in after / before dependencies (#52185) * - * @return array an ordered list of keys + * @return array an ordered list of keys * @throws Exception\InvalidPositionException if the positional string has an unsupported format */ public function getSortedKeys(): array @@ -112,6 +127,8 @@ public function getSortedKeys(): array * Extracts all "middle" keys from $arrayKeysWithPosition. Those are all keys with a numeric position. * The result is a multidimensional arrays where the KEY of each array is a PRIORITY and the VALUE is an array of matching KEYS * This also removes matching keys from the given $arrayKeysWithPosition + * + * @param array $arrayKeysWithPosition */ private function extractMiddleKeys(array &$arrayKeysWithPosition): void { @@ -130,6 +147,8 @@ private function extractMiddleKeys(array &$arrayKeysWithPosition): void * Extracts all "start" keys from $arrayKeysWithPosition. Those are all keys with a position starting with "start" * The result is a multidimensional arrays where the KEY of each array is a PRIORITY and the VALUE is an array of matching KEYS * This also removes matching keys from the given $arrayKeysWithPosition + * + * @param array $arrayKeysWithPosition */ private function extractStartKeys(array &$arrayKeysWithPosition): void { @@ -152,6 +171,8 @@ private function extractStartKeys(array &$arrayKeysWithPosition): void * Extracts all "end" keys from $arrayKeysWithPosition. Those are all keys with a position starting with "end" * The result is a multidimensional arrays where the KEY of each array is a PRIORITY and the VALUE is an array of matching KEYS * This also removes matching keys from the given $arrayKeysWithPosition + * + * @param array $arrayKeysWithPosition */ private function extractEndKeys(array &$arrayKeysWithPosition): void { @@ -174,6 +195,9 @@ private function extractEndKeys(array &$arrayKeysWithPosition): void * Extracts all "before" keys from $arrayKeysWithPosition. Those are all keys with a position starting with "before" * The result is a multidimensional arrays where the KEY of each array is a PRIORITY and the VALUE is an array of matching KEYS * This also removes matching keys from the given $arrayKeysWithPosition + * + * @param array $arrayKeysWithPosition + * @param array $existingKeys */ private function extractBeforeKeys(array &$arrayKeysWithPosition, array $existingKeys): void { @@ -201,6 +225,9 @@ private function extractBeforeKeys(array &$arrayKeysWithPosition, array $existin * Extracts all "after" keys from $arrayKeysWithPosition. Those are all keys with a position starting with "after" * The result is a multidimensional arrays where the KEY of each array is a PRIORITY and the VALUE is an array of matching KEYS * This also removes matching keys from the given $arrayKeysWithPosition + * + * @param array $arrayKeysWithPosition + * @param array $existingKeys */ private function extractAfterKeys(array &$arrayKeysWithPosition, array $existingKeys): void { @@ -228,7 +255,7 @@ private function extractAfterKeys(array &$arrayKeysWithPosition, array $existing * Collect the array keys inside $this->subject with each position meta-argument. * If there is no position but the array is numerically ordered, we use the array index as position. * - * @return array an associative array where each key of $subject has a position string assigned + * @return array an associative array where each key of $subject has a position string assigned */ private function collectArrayKeysAndPositions(): array { @@ -253,6 +280,8 @@ private function collectArrayKeysAndPositions(): array /** * Flattens start-, middle-, end-, before- and afterKeys to a single dimension and merges them together to a single array + * + * @return array */ private function generateSortedKeysMap(): array { diff --git a/Neos.Utility.Arrays/Classes/ValueAccessor.php b/Neos.Utility.Arrays/Classes/ValueAccessor.php index 060ee0c8d7..5bfb0e7852 100644 --- a/Neos.Utility.Arrays/Classes/ValueAccessor.php +++ b/Neos.Utility.Arrays/Classes/ValueAccessor.php @@ -47,6 +47,7 @@ public static function forValue(mixed $value): self /** * @internal You should use {@see ValueAccessor::forValue} instead + * @param array $path */ public static function forValueInPath(mixed $value, array|string $path): self { @@ -97,6 +98,9 @@ public function classString(): string throw $this->createTypeError("is not a class-string"); } + /** + * @return array + */ public function array(): array { if (is_array($this->value)) { @@ -161,6 +165,9 @@ public function classStringOrNull(): ?string throw $this->createTypeError("is not a class-string"); } + /** + * @return array|null + */ public function arrayOrNull(): ?array { if (is_array($this->value) || is_null($this->value)) { @@ -182,7 +189,7 @@ public function instanceOfOrNull(string $className): ?object throw $this->createTypeError(sprintf('is not an instance of %s or null', $className)); } - private function createTypeError($message): \UnexpectedValueException + private function createTypeError(string $message): \UnexpectedValueException { return new \UnexpectedValueException(get_debug_type($this->value) . ' ' . $message . ($this->additionalErrorMessage ? ' ' . $this->additionalErrorMessage : '')); } diff --git a/Neos.Utility.Files/Classes/Files.php b/Neos.Utility.Files/Classes/Files.php index b0ff9b5b91..136bbed8e4 100644 --- a/Neos.Utility.Files/Classes/Files.php +++ b/Neos.Utility.Files/Classes/Files.php @@ -32,7 +32,7 @@ public static function getUnixStylePath(string $path): string if (strpos($path, ':') === false) { return str_replace(['//', '\\'], '/', $path); } - return preg_replace('/^([a-z]{2,}):\//', '$1://', str_replace(['//', '\\'], '/', $path)); + return preg_replace('/^([a-z]{2,}):\//', '$1://', str_replace(['//', '\\'], '/', $path)) ?: ''; } /** @@ -53,7 +53,7 @@ public static function getNormalizedPath(string $path): string * Note: trailing slashes will be removed, leading slashes won't. * Usage: concatenatePaths(array('dir1/dir2', 'dir3', 'file')) * - * @param array $paths the file paths to be combined. Last array element may include the filename. + * @param array $paths the file paths to be combined. Last array element may include the filename. * @return string concatenated path without trailing slash. * @see getUnixStylePath() * @api @@ -80,10 +80,10 @@ public static function concatenatePaths(array $paths): string * directories. * * @param string $path Path to the directory which shall be read - * @param string $suffix If specified, only filenames with this extension are returned (eg. ".php" or "foo.bar") + * @param ?string $suffix If specified, only filenames with this extension are returned (eg. ".php" or "foo.bar") * @param boolean $returnRealPath If turned on, all paths are resolved by calling realpath() * @param boolean $returnDotFiles If turned on, also files beginning with a dot will be returned - * @return array Filenames including full path + * @return array Filenames including full path * @api */ public static function readDirectoryRecursively(string $path, ?string $suffix = null, bool $returnRealPath = false, bool $returnDotFiles = false): array @@ -122,7 +122,7 @@ public static function getRecursiveDirectoryGenerator(string $path, ?string $suf if (is_dir($pathAndFilename)) { array_push($directories, self::getNormalizedPath($pathAndFilename)); } elseif ($suffix === null || strpos(strrev($filename), strrev($suffix)) === 0) { - yield static::getUnixStylePath(($returnRealPath === true) ? realpath($pathAndFilename) : $pathAndFilename); + yield static::getUnixStylePath(($returnRealPath === true) ? realpath($pathAndFilename) ?: '' : $pathAndFilename); } } closedir($handle); @@ -153,6 +153,9 @@ public static function emptyDirectoryRecursively(string $path) } else { $directoryIterator = new \RecursiveDirectoryIterator($path); foreach ($directoryIterator as $fileInfo) { + if (is_string($fileInfo)) { + continue; + } if (!$fileInfo->isDir()) { if (self::unlink($fileInfo->getPathname()) !== true) { throw new FilesException('Could not unlink file "' . $fileInfo->getPathname() . '".', 1169047619); @@ -322,14 +325,12 @@ public static function copyDirectoryRecursively(string $sourceDirectory, string */ public static function getFileContents(string $pathAndFilename, int $flags = 0, $context = null, int $offset = 0, int $maximumLength = -1) { - if ($flags === true) { - $flags = FILE_USE_INCLUDE_PATH; - } + $useIncludePath = ($flags & FILE_USE_INCLUDE_PATH) === 1; try { if ($maximumLength > -1) { - $content = file_get_contents($pathAndFilename, $flags, $context, $offset, $maximumLength); + $content = file_get_contents($pathAndFilename, $useIncludePath, $context, $offset, $maximumLength); } else { - $content = file_get_contents($pathAndFilename, $flags, $context, $offset); + $content = file_get_contents($pathAndFilename, $useIncludePath, $context, $offset); } } catch (ErrorException $ignoredException) { $content = false; @@ -386,7 +387,7 @@ public static function is_link(string $pathAndFilename): bool return false; } $normalizedPathAndFilename = strtolower(rtrim(self::getUnixStylePath($pathAndFilename), '/')); - $normalizedTargetPathAndFilename = strtolower(self::getUnixStylePath(realpath($pathAndFilename))); + $normalizedTargetPathAndFilename = strtolower(self::getUnixStylePath(realpath($pathAndFilename) ?: '')); if ($normalizedTargetPathAndFilename === '') { return false; } @@ -431,20 +432,19 @@ public static function unlink(string $pathAndFilename): bool /** * Supported file size units for the byte conversion functions below * - * @var array + * @var array */ - protected static $sizeUnits = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + protected static array $sizeUnits = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; /** * Converts an integer with a byte count into human-readable form * - * @param float|integer $bytes - * @param integer $decimals number of decimal places in the resulting string - * @param string $decimalSeparator decimal separator of the resulting string - * @param string $thousandsSeparator thousands separator of the resulting string + * @param integer|null $decimals number of decimal places in the resulting string + * @param string|null $decimalSeparator decimal separator of the resulting string + * @param string|null $thousandsSeparator thousands separator of the resulting string * @return string the size string, e.g. "1,024 MB" */ - public static function bytesToSizeString($bytes, ?int $decimals = null, ?string $decimalSeparator = null, ?string $thousandsSeparator = null): string + public static function bytesToSizeString(float|int|string $bytes, ?int $decimals = null, ?string $decimalSeparator = null, ?string $thousandsSeparator = null): string { if (!is_int($bytes) && !is_float($bytes)) { if (is_numeric($bytes)) { @@ -502,7 +502,7 @@ public static function sizeStringToBytes(string $sizeString): float if ($pow === false) { throw new FilesException(sprintf('Unknown file size unit "%s"', $matches['unit']), 1417695299); } - return $size * pow(2, (10 * $pow)); + return $size * pow(2, (10 * (int)$pow)); } /** diff --git a/Neos.Utility.MediaTypes/Classes/MediaTypes.php b/Neos.Utility.MediaTypes/Classes/MediaTypes.php index 249d21ced8..65c0db4f0c 100644 --- a/Neos.Utility.MediaTypes/Classes/MediaTypes.php +++ b/Neos.Utility.MediaTypes/Classes/MediaTypes.php @@ -33,7 +33,7 @@ abstract class MediaTypes /** * A map of file extensions to Internet Media Types * - * @var array + * @var array */ private static $extensionToMediaType = [ '3dml' => 'text/vnd.in3d.3dml', @@ -1027,7 +1027,7 @@ abstract class MediaTypes /** * A map of Internet Media Types to file extensions * - * @var array + * @var array> */ private static $mediaTypeToFileExtension = [ 'application/andrew-inset' => ['ez'], @@ -1821,13 +1821,13 @@ public static function getMediaTypeFromFilename(string $filename): string * Returns a Media Type based on the file content * * @param string $fileContent The file content do determine the media type from - * @return string The IANA Internet Media Type + * @return string|null The IANA Internet Media Type */ - public static function getMediaTypeFromFileContent(string $fileContent): string + public static function getMediaTypeFromFileContent(string $fileContent): ?string { $fileInfo = new \finfo(FILEINFO_MIME); - $mediaType = self::trimMediaType($fileInfo->buffer($fileContent)); - return isset(self::$mediaTypeToFileExtension[$mediaType]) ? $mediaType : 'application/octet-stream'; + $mediaType = self::trimMediaType($fileInfo->buffer($fileContent) ?: ''); + return ($mediaType === null || isset(self::$mediaTypeToFileExtension[$mediaType])) ? $mediaType : 'application/octet-stream'; } /** @@ -1846,7 +1846,7 @@ public static function getFilenameExtensionFromMediaType(string $mediaType): str * Returns all possible filename extensions based on the given Media Type. * * @param string $mediaType The IANA Internet Media Type, for example "text/html" - * @return array The corresponding filename extensions, for example ("html", "htm") + * @return array The corresponding filename extensions, for example ("html", "htm") * @api */ public static function getFilenameExtensionsFromMediaType(string $mediaType): array @@ -1867,7 +1867,7 @@ public static function getFilenameExtensionsFromMediaType(string $mediaType): ar * "parameters" => an array of parameter names and values, array("charset" => "UTF-8") * * @param string $rawMediaType The raw media type, for example "application/json; charset=UTF-8" - * @return array An associative array with parsed information + * @return array{type: string, subtype: string, parameters: array} An associative array with parsed information */ public static function parseMediaType(string $rawMediaType): array { @@ -1921,9 +1921,9 @@ public static function mediaRangeMatches(string $mediaRange, string $mediaType): * and subtype in the format "type/subtype". * * @param string $rawMediaType The full media type, for example "application/json; charset=UTF-8" - * @return string Just the type and subtype, for example "application/json" + * @return string|null Just the type and subtype, for example "application/json" */ - public static function trimMediaType(string $rawMediaType) + public static function trimMediaType(string $rawMediaType): ?string { $pieces = self::parseMediaType($rawMediaType); return trim(sprintf('%s/%s', $pieces['type'], $pieces['subtype']), '/') ?: null; diff --git a/Neos.Utility.ObjectHandling/Classes/ObjectAccess.php b/Neos.Utility.ObjectHandling/Classes/ObjectAccess.php index 89decda676..cb15a28b43 100644 --- a/Neos.Utility.ObjectHandling/Classes/ObjectAccess.php +++ b/Neos.Utility.ObjectHandling/Classes/ObjectAccess.php @@ -31,15 +31,15 @@ abstract class ObjectAccess { /** * Internal RuntimeCache for getGettablePropertyNames() - * @var array + * @var array,array> */ - protected static $gettablePropertyNamesCache = []; + protected static array $gettablePropertyNamesCache = []; /** * Internal RuntimeCache for getPropertyInternal() - * @var array + * @var array */ - protected static $propertyGetterCache = []; + protected static array $propertyGetterCache = []; const ACCESS_GET = 0; const ACCESS_SET = 1; @@ -58,22 +58,14 @@ abstract class ObjectAccess * - if public property exists, return the value of it. * - else, throw exception * - * @param mixed $subject Object or array to get the property from + * @param object|array $subject Object or array to get the property from * @param string|integer $propertyName Name or index of the property to retrieve * @param boolean $forceDirectAccess Directly access property using reflection(!) * @return mixed Value of the property - * @throws \InvalidArgumentException in case $subject was not an object or $propertyName was not a string * @throws PropertyNotAccessibleException if the property was not accessible */ - public static function getProperty($subject, $propertyName, bool $forceDirectAccess = false) + public static function getProperty(object|array $subject, string|int $propertyName, bool $forceDirectAccess = false) { - if (!is_object($subject) && !is_array($subject)) { - throw new \InvalidArgumentException('$subject must be an object or array, ' . gettype($subject) . ' given.', 1237301367); - } - if (!is_string($propertyName) && !is_int($propertyName)) { - throw new \InvalidArgumentException('Given property name/index is not of type string or integer.', 1231178303); - } - $propertyExists = false; $propertyValue = self::getPropertyInternal($subject, $propertyName, $forceDirectAccess, $propertyExists); if ($propertyExists === true) { @@ -91,14 +83,14 @@ public static function getProperty($subject, $propertyName, bool $forceDirectAcc * of type string you should use getProperty() instead. * * @param mixed $subject Object or array to get the property from - * @param string $propertyName name of the property to retrieve + * @param string|int $propertyName name of the property to retrieve * @param boolean $forceDirectAccess directly access property using reflection(!) * @param boolean $propertyExists (by reference) will be set to true if the specified property exists and is gettable * @return mixed Value of the property * @throws PropertyNotAccessibleException * @see getProperty() */ - protected static function getPropertyInternal($subject, string $propertyName, bool $forceDirectAccess, bool &$propertyExists) + protected static function getPropertyInternal($subject, string|int $propertyName, bool $forceDirectAccess, bool &$propertyExists) { if ($subject === null) { return null; @@ -111,7 +103,12 @@ protected static function getPropertyInternal($subject, string $propertyName, bo return null; } + if (is_int($propertyName)) { + throw new \InvalidArgumentException('Cannot use integer property names for objects', 1743799241); + } + $propertyExists = true; + /** @var string $className safe for objects */ $className = TypeHandling::getTypeForValue($subject); if ($forceDirectAccess === true) { @@ -230,39 +227,33 @@ public static function getPropertyPath($subject, string $propertyPath) * on it without checking if it existed. * - else, return false * - * @param mixed $subject The target object or array + * @template T of object + * @param array|T &$subject The target object or array * @param string|integer $propertyName Name or index of the property to set * @param mixed $propertyValue Value of the property * @param boolean $forceDirectAccess directly access property using reflection(!) + * @param-out ($subject is array ? array : T) $subject * @return boolean true if the property could be set, false otherwise - * @throws \InvalidArgumentException in case $object was not an object or $propertyName was not a string */ - public static function setProperty(&$subject, $propertyName, $propertyValue, bool $forceDirectAccess = false): bool + public static function setProperty(array|object &$subject, string|int $propertyName, $propertyValue, bool $forceDirectAccess = false): bool { - if (!is_string($propertyName) && !is_int($propertyName)) { - throw new \InvalidArgumentException('Given property name/index is not of type string or integer.', 1231178878); - } - if (is_array($subject)) { $subject[$propertyName] = $propertyValue; return true; } - if (!is_object($subject)) { - throw new \InvalidArgumentException('subject must be an object or array, ' . gettype($subject) . ' given.', 1237301368); - } - if ($forceDirectAccess === true) { if (!is_string($propertyName)) { throw new \InvalidArgumentException('Given property name is not of type string.', 1648244846); } + /** @var string $className safe for objects */ $className = TypeHandling::getTypeForValue($subject); if (property_exists($className, $propertyName)) { $propertyReflection = new \ReflectionProperty($className, $propertyName); $propertyReflection->setAccessible(true); $propertyReflection->setValue($subject, $propertyValue); - } elseif ($subject instanceof ProxyInterface && property_exists(get_parent_class($className), $propertyName)) { - $propertyReflection = new \ReflectionProperty(get_parent_class($className), $propertyName); + } elseif ($subject instanceof ProxyInterface && property_exists(get_parent_class($className) ?: $className, $propertyName)) { + $propertyReflection = new \ReflectionProperty(get_parent_class($className) ?: $className, $propertyName); $propertyReflection->setAccessible(true); $propertyReflection->setValue($subject, $propertyValue); } else { @@ -290,19 +281,17 @@ public static function setProperty(&$subject, $propertyName, $propertyValue, boo * - public properties which can be directly get. * * @param object $object Object to receive property names for - * @return array Array of all gettable property names + * @return array Array of all gettable property names * @throws \InvalidArgumentException */ - public static function getGettablePropertyNames($object): array + public static function getGettablePropertyNames(object $object): array { - if (!is_object($object)) { - throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1237301369); - } if ($object instanceof \stdClass) { $declaredPropertyNames = array_keys(get_object_vars($object)); $className = 'stdClass'; unset(self::$gettablePropertyNamesCache[$className]); } else { + /** @var string $className safe for objects */ $className = TypeHandling::getTypeForValue($object); $declaredPropertyNames = array_keys(get_class_vars($className)); } @@ -336,17 +325,15 @@ public static function getGettablePropertyNames($object): array * - public properties which can be directly set. * * @param object $object Object to receive property names for - * @return array Array of all settable property names + * @return array Array of all settable property names * @throws \InvalidArgumentException */ - public static function getSettablePropertyNames($object): array + public static function getSettablePropertyNames(object $object): array { - if (!is_object($object)) { - throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1264022994); - } if ($object instanceof \stdClass) { $declaredPropertyNames = array_keys(get_object_vars($object)); } else { + /** @var string $className safe for object */ $className = TypeHandling::getTypeForValue($object); $declaredPropertyNames = array_keys(get_class_vars($className)); } @@ -370,12 +357,9 @@ public static function getSettablePropertyNames($object): array * @return boolean * @throws \InvalidArgumentException */ - public static function isPropertySettable($object, string $propertyName): bool + public static function isPropertySettable(object $object, string $propertyName): bool { - if (!is_object($object)) { - throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1259828920); - } - + /** @var string $className safe for objects */ $className = TypeHandling::getTypeForValue($object); if (($object instanceof \stdClass && array_key_exists($propertyName, get_object_vars($object))) || array_key_exists($propertyName, get_class_vars($className))) { return true; @@ -391,18 +375,19 @@ public static function isPropertySettable($object, string $propertyName): bool * @return boolean * @throws \InvalidArgumentException */ - public static function isPropertyGettable($object, string $propertyName): bool + public static function isPropertyGettable(object $object, string $propertyName): bool { - if (!is_object($object)) { - throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1259828921); - } - if (($object instanceof \ArrayAccess && $object->offsetExists($propertyName)) || ($object instanceof \stdClass && array_key_exists($propertyName, get_object_vars($object)))) { + if ( + $object instanceof \ArrayAccess && $object->offsetExists($propertyName) + || $object instanceof \stdClass && array_key_exists($propertyName, get_object_vars($object)) + ) { return true; } $uppercasePropertyName = ucfirst($propertyName); if (is_callable([$object, 'get' . $uppercasePropertyName]) || is_callable([$object, 'is' . $uppercasePropertyName]) || is_callable([$object, 'has' . $uppercasePropertyName])) { return true; } + /** @var string $className safe for objects */ $className = TypeHandling::getTypeForValue($object); return array_key_exists($propertyName, get_class_vars($className)); } @@ -412,15 +397,11 @@ public static function isPropertyGettable($object, string $propertyName): bool * $object that are accessible through this class. * * @param object $object Object to get all properties from. - * @return array Associative array of all properties. - * @throws \InvalidArgumentException + * @return array Associative array of all properties. * @todo What to do with ArrayAccess */ - public static function getGettableProperties($object): array + public static function getGettableProperties(object $object): array { - if (!is_object($object)) { - throw new \InvalidArgumentException('$object must be an object, ' . gettype($object) . ' given.', 1237301370); - } $properties = []; foreach (self::getGettablePropertyNames($object) as $propertyName) { $propertyExists = false; diff --git a/Neos.Utility.ObjectHandling/Classes/TypeHandling.php b/Neos.Utility.ObjectHandling/Classes/TypeHandling.php index c078b86b71..77fc5050a0 100644 --- a/Neos.Utility.ObjectHandling/Classes/TypeHandling.php +++ b/Neos.Utility.ObjectHandling/Classes/TypeHandling.php @@ -31,16 +31,16 @@ abstract class TypeHandling const LITERAL_TYPE_PATTERN = '/^(?:integer|int|float|double|boolean|bool|string)$/'; /** - * @var array + * @var array>> */ - protected static $collectionTypes = ['array', \Traversable::class]; + protected static array $collectionTypes = ['array', \Traversable::class]; /** * Returns an array with type information, including element type for * collection types (array, SplObjectStorage, ...) * * @param string $type Type of the property (see PARSE_TYPE_PATTERN) - * @return array An array with information about the type + * @return array{type: string, elementType: ?string, nullable: bool} An array with information about the type * @throws InvalidTypeException */ public static function parseType(string $type): array @@ -52,7 +52,7 @@ public static function parseType(string $type): array $typeWithoutNull = self::stripNullableType($type); $isNullable = $typeWithoutNull !== $type || $type === 'null'; - $type = self::normalizeType($matches['type']); + $type = self::normalizeType($matches['type'] ?? ''); $elementType = isset($matches['elementType']) ? self::normalizeType($matches['elementType']) : null; if ($elementType !== null && !self::isCollectionType($type)) { @@ -116,7 +116,7 @@ public static function isSimpleType(string $type): bool /** * Returns true if the $type is a collection type. * - * @param string $type + * @param string|class-string $type * @return boolean */ public static function isCollectionType(string $type): bool @@ -127,6 +127,7 @@ public static function isCollectionType(string $type): bool if (class_exists($type) === true || interface_exists($type) === true) { foreach (self::$collectionTypes as $collectionType) { + /** @phpstan-ignore function.alreadyNarrowedType, identical.alwaysTrue (this can be false) */ if (is_subclass_of($type, $collectionType) === true) { return true; } @@ -185,21 +186,23 @@ public static function stripNullableType($type) if (stripos($type, 'null') === false) { return $type; } - return preg_replace('/(\\|null|null\\|)/i', '', $type); + return preg_replace('/(\\|null|null\\|)/i', '', $type) ?: ''; } /** * Return simple type or class for object * * @param mixed $value - * @return string + * @return ($value is object ? class-string : string) */ public static function getTypeForValue($value): string { if (is_object($value)) { if ($value instanceof Proxy) { + /** @var class-string $type */ $type = get_parent_class($value); } else { + /** @var class-string $type */ $type = get_class($value); } } else { diff --git a/Neos.Utility.Pdo/Classes/PdoHelper.php b/Neos.Utility.Pdo/Classes/PdoHelper.php index 873ef34621..40ac1dac76 100644 --- a/Neos.Utility.Pdo/Classes/PdoHelper.php +++ b/Neos.Utility.Pdo/Classes/PdoHelper.php @@ -33,7 +33,7 @@ abstract class PdoHelper * @param \PDO $databaseHandle * @param string $pdoDriver * @param string $pathAndFilename - * @param array $replacePairs every key in this array will be replaced with the corresponding value in the loaded SQL (example: ['###CACHE_TABLE_NAME###' => 'caches', '###TAGS_TABLE_NAME###' => 'tags']) + * @param array $replacePairs every key in this array will be replaced with the corresponding value in the loaded SQL (example: ['###CACHE_TABLE_NAME###' => 'caches', '###TAGS_TABLE_NAME###' => 'tags']) * @return void */ public static function importSql(\PDO $databaseHandle, string $pdoDriver, string $pathAndFilename, array $replacePairs = []) @@ -46,13 +46,13 @@ public static function importSql(\PDO $databaseHandle, string $pdoDriver, string } else { $sql = file($pathAndFilename, FILE_IGNORE_NEW_LINES & FILE_SKIP_EMPTY_LINES); // Remove MySQL style key length delimiters (yuck!) if we are not setting up a MySQL db - if ($pdoDriver !== 'mysql') { + if ($sql !== false && $pdoDriver !== 'mysql') { $sql = preg_replace('/"\([0-9]+\)/', '"', $sql); } } $statement = ''; - foreach ($sql as $line) { + foreach ($sql ?: [] as $line) { $statement .= ' ' . trim(strtr($line, $replacePairs)); if (substr($statement, -1) === ';') { $databaseHandle->exec($statement); diff --git a/Neos.Utility.Schema/Classes/SchemaGenerator.php b/Neos.Utility.Schema/Classes/SchemaGenerator.php index 07a977c120..5ac82327d2 100644 --- a/Neos.Utility.Schema/Classes/SchemaGenerator.php +++ b/Neos.Utility.Schema/Classes/SchemaGenerator.php @@ -26,7 +26,7 @@ class SchemaGenerator * Generate a schema for the given value * * @param mixed $value value to create a schema for - * @return array schema as array structure + * @return array schema as array structure */ public function generate($value): array { @@ -62,8 +62,8 @@ public function generate($value): array /** * Create a schema for a dictionary * - * @param array $dictionaryValue - * @return array + * @param array $dictionaryValue + * @return array{type: string, properties: array} */ protected function generateDictionarySchema(array $dictionaryValue): array { @@ -78,8 +78,8 @@ protected function generateDictionarySchema(array $dictionaryValue): array /** * Create a schema for an array structure * - * @param array $arrayValue - * @return array schema + * @param array $arrayValue + * @return array schema */ protected function generateArraySchema(array $arrayValue): array { @@ -96,7 +96,7 @@ protected function generateArraySchema(array $arrayValue): array * Create a schema for a given string * * @param string $stringValue - * @return array + * @return array */ protected function generateStringSchema(string $stringValue): array { @@ -122,7 +122,7 @@ protected function generateStringSchema(string $stringValue): array * Compact an array of items to avoid adding the same value more than once. * If the result contains only one item, that item is returned directly. * - * @param array $values array of values + * @param array $values array of values * @return mixed */ protected function filterDuplicatesFromArray(array $values) diff --git a/Neos.Utility.Schema/Classes/SchemaValidator.php b/Neos.Utility.Schema/Classes/SchemaValidator.php index 64fec01df2..82427d9827 100644 --- a/Neos.Utility.Schema/Classes/SchemaValidator.php +++ b/Neos.Utility.Schema/Classes/SchemaValidator.php @@ -69,7 +69,7 @@ class SchemaValidator * * @param mixed $value value to validate * @param mixed $schema type as string, schema or array of schemas - * @param array $types the additional type schemas + * @param array $types the additional type schemas * @return ErrorResult */ public function validate($value, $schema, array $types = []): ErrorResult @@ -147,8 +147,8 @@ public function validate($value, $schema, array $types = []): ErrorResult * Validate a value for a given type * * @param mixed $value - * @param array $schema - * @param array $types + * @param array $schema + * @param array $types * @return ErrorResult */ protected function validateType($value, array $schema, array $types = []): ErrorResult @@ -209,8 +209,8 @@ protected function validateType($value, array $schema, array $types = []): Error * Validate a value with a given list of allowed types * * @param mixed $value - * @param array $schema - * @param array $types + * @param array $schema + * @param array $types * @return ErrorResult */ protected function validateTypeArray($value, array $schema, array $types = []): ErrorResult @@ -254,7 +254,7 @@ protected function validateTypeArray($value, array $schema, array $types = []): * - divisibleBy : value is divisibleBy the given number * * @param mixed $value - * @param array $schema + * @param array $schema * @return ErrorResult */ protected function validateNumberType($value, array $schema): ErrorResult @@ -305,7 +305,7 @@ protected function validateNumberType($value, array $schema): ErrorResult * * @see SchemaValidator::validateNumberType * @param mixed $value - * @param array $schema + * @param array $schema * @return ErrorResult */ protected function validateIntegerType($value, array $schema): ErrorResult @@ -323,7 +323,7 @@ protected function validateIntegerType($value, array $schema): ErrorResult * Validate a boolean value with the given schema * * @param mixed $value - * @param array $schema + * @param array $schema * @return ErrorResult */ protected function validateBooleanType($value, array $schema): ErrorResult @@ -346,8 +346,8 @@ protected function validateBooleanType($value, array $schema): ErrorResult * - uniqueItems : allow only unique values * * @param mixed $value - * @param array $schema - * @param array $types + * @param array $schema + * @param array $types * @return ErrorResult */ protected function validateArrayType($value, array $schema, array $types = []): ErrorResult @@ -404,8 +404,8 @@ protected function validateArrayType($value, array $schema, array $types = []): * - additionalProperties : if false is given all additionalProperties are forbidden, if a schema is given all additional properties have to match the schema * * @param mixed $value - * @param array $schema - * @param array $types + * @param array $schema + * @param array $types * @return ErrorResult */ protected function validateDictionaryType($value, array $schema, array $types = []): ErrorResult @@ -496,8 +496,8 @@ protected function validateDictionaryType($value, array $schema, array $types = * [date-time|date|time|uri|email|ipv4|ipv6|ip-address|host-name|class-name|interface-name] * * @param mixed $value - * @param array $schema - * @param array $types + * @param array $schema + * @param array $types * @return ErrorResult */ protected function validateStringType($value, array $schema, array $types = []): ErrorResult @@ -530,6 +530,7 @@ protected function validateStringType($value, array $schema, array $types = []): switch ($schema['format']) { case 'date-time': // YYYY-MM-DDThh:mm:ssZ ISO8601 + /** @phpstan-ignore staticMethod.resultUnused (we only care about the error) */ \DateTime::createFromFormat(\DateTime::ISO8601, $value); $parseErrors = \DateTime::getLastErrors(); if ($parseErrors && $parseErrors['error_count'] > 0) { @@ -538,6 +539,7 @@ protected function validateStringType($value, array $schema, array $types = []): break; case 'date': // YYYY-MM-DD + /** @phpstan-ignore staticMethod.resultUnused (we only care about the error) */ \DateTime::createFromFormat('Y-m-d', $value); $parseErrors = \DateTime::getLastErrors(); if ($parseErrors && $parseErrors['error_count'] > 0) { @@ -546,6 +548,7 @@ protected function validateStringType($value, array $schema, array $types = []): break; case 'time': // hh:mm:ss + /** @phpstan-ignore staticMethod.resultUnused (we only care about the error) */ \DateTime::createFromFormat('H:i:s', $value); $parseErrors = \DateTime::getLastErrors(); if ($parseErrors && $parseErrors['error_count'] > 0) { @@ -604,7 +607,7 @@ protected function validateStringType($value, array $schema, array $types = []): * Validate a null value with the given schema * * @param mixed $value - * @param array $schema + * @param array $schema * @return ErrorResult */ protected function validateNullType($value, array $schema): ErrorResult @@ -620,7 +623,7 @@ protected function validateNullType($value, array $schema): ErrorResult * Validate any value with the given schema. Return always a valid Result. * * @param mixed $value - * @param array $schema + * @param array $schema * @return ErrorResult */ protected function validateAnyType($value, array $schema): ErrorResult @@ -665,7 +668,7 @@ protected function createError(string $expectation, $value = null): Error * Determine whether the given php array is a schema or not * * @todo there should be a more sophisticated way to detect schemas - * @param array $phpArray + * @param array $phpArray * @return boolean */ protected function isSchema(array $phpArray): bool @@ -677,7 +680,7 @@ protected function isSchema(array $phpArray): bool /** * Determine whether the given php array is a plain numerically indexed array * - * @param array $phpArray + * @param array $phpArray * @return boolean */ protected function isNumericallyIndexedArray(array $phpArray): bool @@ -693,7 +696,7 @@ protected function isNumericallyIndexedArray(array $phpArray): bool /** * Determine whether the given php array is a Dictionary (has no numeric identifiers) * - * @param array $phpArray + * @param array $phpArray * @return boolean */ protected function isDictionary(array $phpArray): bool diff --git a/Neos.Utility.Unicode/Classes/Functions.php b/Neos.Utility.Unicode/Classes/Functions.php index a13429ebb5..806227f83d 100644 --- a/Neos.Utility.Unicode/Classes/Functions.php +++ b/Neos.Utility.Unicode/Classes/Functions.php @@ -31,7 +31,7 @@ public static function strtotitle(string $string): string { $result = ''; $splitIntoLowerCaseWords = preg_split("/([\n\r\t ])/", self::strtolower($string), -1, PREG_SPLIT_DELIM_CAPTURE); - foreach ($splitIntoLowerCaseWords as $delimiterOrValue) { + foreach ($splitIntoLowerCaseWords ?: [] as $delimiterOrValue) { $result .= self::strtoupper(self::substr($delimiterOrValue, 0, 1)) . self::substr($delimiterOrValue, 1); } return $result; @@ -133,7 +133,7 @@ public static function lcfirst(string $string): string * @param string $haystack UTF-8 string to search in * @param string $needle UTF-8 string to search for * @param integer $offset Positition to start the search - * @return integer The character position + * @return integer|false The character position * @api */ public static function strpos(string $haystack, string $needle, int $offset = 0) @@ -154,16 +154,19 @@ public static function strpos(string $haystack, string $needle, int $offset = 0) * @see http://www.php.net/manual/en/function.pathinfo.php * * @param string $path - * @param integer $options Optional, one of PATHINFO_DIRNAME, PATHINFO_BASENAME, PATHINFO_EXTENSION or PATHINFO_FILENAME. - * @return string|array + * @param integer|null $options Optional, one of PATHINFO_DIRNAME, PATHINFO_BASENAME, PATHINFO_EXTENSION or PATHINFO_FILENAME. + * @return ($options is null ? array{dirname: string, basename: string, extension: string, filename: string} : string) * @api */ - public static function pathinfo(string $path, ?int $options = null) + public static function pathinfo(string $path, ?int $options = null): string|array { + /** @var string $currentLocale we assume the current locale to be properly set */ + /** @phpstan-ignore argument.type (int is allowed for locales) */ $currentLocale = setlocale(LC_CTYPE, 0); // Before we have a setting for setlocale, his should suffice for pathinfo // to work correctly on Unicode strings setlocale(LC_CTYPE, 'en_US.UTF-8'); + /** @var string|array{dirname: string, basename: string, extension: string, filename: string} $pathinfo */ $pathinfo = $options == null ? pathinfo($path) : pathinfo($path, $options); setlocale(LC_CTYPE, $currentLocale); return $pathinfo; @@ -188,8 +191,11 @@ public static function parse_url(string $url, int $component = -1) $encodedUrl = preg_replace_callback('%[^:@/?#&=\.]+%usD', function ($matches) { return urlencode($matches[0]); }, $url); - $components = parse_url($encodedUrl); + if ($encodedUrl === null) { + return false; + } + $components = parse_url($encodedUrl); if ($components === false) { return false; } diff --git a/Neos.Utility.Unicode/Classes/TextIterator.php b/Neos.Utility.Unicode/Classes/TextIterator.php index 862a05c204..2af09dd48d 100644 --- a/Neos.Utility.Unicode/Classes/TextIterator.php +++ b/Neos.Utility.Unicode/Classes/TextIterator.php @@ -19,6 +19,7 @@ /** * A UTF8-aware TextIterator * + * @implements \Iterator */ class TextIterator implements \Iterator { @@ -68,12 +69,12 @@ class TextIterator implements \Iterator protected $currentPosition; /** - * @var \ArrayObject + * @var \ArrayObject */ protected $iteratorCache; /** - * @var \ArrayIterator + * @var \ArrayIterator */ protected $iteratorCacheIterator; @@ -109,11 +110,11 @@ public function __construct(string $subject, int $iteratorType = self::CHARACTER /** * Returns the current element * - * @return string The value of the current element + * @return string|null The value of the current element */ - public function current(): string + public function current(): ?string { - return $this->getCurrentElement()->getValue(); + return $this->getCurrentElement()?->getValue(); } /** @@ -123,7 +124,7 @@ public function current(): string */ public function next(): void { - $this->previousElement = $this->getCurrentElement(); + $this->previousElement = $this->getCurrentElement() ?: $this->previousElement; $this->iteratorCacheIterator->next(); } @@ -164,11 +165,11 @@ public function rewind(): void /** * Returns the offset in the original given string of the current element * - * @return integer The offset of the current element + * @return int|null The offset of the current element, if any */ - public function offset(): int + public function offset(): ?int { - return $this->getCurrentElement()->getOffset(); + return $this->getCurrentElement()?->getOffset(); } /** @@ -194,7 +195,8 @@ public function last(): string $previousElement = $this->getCurrentElement(); $this->next(); } - return $previousElement->getValue(); + // can never be null because the iterator cannot be empty + return $previousElement?->getValue() ?: ''; } /** @@ -202,15 +204,15 @@ public function last(): string * given by its offset * * @param integer $offset The offset of the character - * @return int The offset of the element following this character + * @return int|null The offset of the element following this character or null if the offset is out of bounds */ - public function following(int $offset): int + public function following(int $offset): ?int { $this->rewind(); while ($this->valid()) { $this->next(); $nextElement = $this->getCurrentElement(); - if ($nextElement->getOffset() >= $offset) { + if ($nextElement && $nextElement->getOffset() >= $offset) { return $nextElement->getOffset(); } } @@ -231,7 +233,7 @@ public function preceding(int $offset): int $previousElement = $this->getCurrentElement(); $this->next(); $currentElement = $this->getCurrentElement(); - if (($currentElement->getOffset() + $currentElement->getLength()) >= $offset) { + if ($previousElement && $currentElement && ($currentElement->getOffset() + $currentElement->getLength()) >= $offset) { return $previousElement->getOffset() + $previousElement->getLength(); } } @@ -254,20 +256,21 @@ public function preceding(int $offset): int */ public function isBoundary(): bool { - return $this->getCurrentElement()->isBoundary(); + return $this->getCurrentElement()?->isBoundary() ?: false; } /** * Returns all elements of the iterator in an array * - * @return array All elements of the iterator + * @return array All elements of the iterator */ public function getAll(): array { $this->rewind(); $allValues = []; while ($this->valid()) { - $allValues[] = $this->getCurrentElement()->getValue(); + // will never be null while valid + $allValues[] = $this->getCurrentElement()?->getValue() ?: ''; $this->next(); } return $allValues; @@ -276,7 +279,7 @@ public function getAll(): array /** * @throws UnsupportedFeatureException */ - public function getRuleStatus() + public function getRuleStatus(): never { throw new UnsupportedFeatureException('getRuleStatus() is not supported.', 1210849057); } @@ -284,7 +287,7 @@ public function getRuleStatus() /** * @throws UnsupportedFeatureException */ - public function getRuleStatusArray() + public function getRuleStatusArray(): never { throw new UnsupportedFeatureException('getRuleStatusArray() is not supported.', 1210849076); } @@ -292,7 +295,7 @@ public function getRuleStatusArray() /** * @throws UnsupportedFeatureException */ - public function getAvailableLocales() + public function getAvailableLocales(): never { throw new UnsupportedFeatureException('getAvailableLocales() is not supported.', 1210849105); } @@ -305,7 +308,8 @@ public function getAvailableLocales() public function first(): string { $this->rewind(); - return $this->getCurrentElement()->getValue(); + // can never be empty, will at least contain empty string + return $this->getCurrentElement()?->getValue() ?: ''; } /** @@ -346,7 +350,7 @@ private function generateIteratorElements() private function parseSubjectByCharacter(): void { $i = 0; - foreach (preg_split('//u', $this->subject) as $currentCharacter) { + foreach (preg_split('//u', $this->subject) ?: [] as $currentCharacter) { if ($currentCharacter === '') { continue; } @@ -373,7 +377,7 @@ private function parseSubjectByWord() $this->iteratorCache->append(new TextIteratorElement(' ', $i, 1, true)); $j = 0; - $splittedWord = preg_split('/' . self::REGEXP_SENTENCE_DELIMITERS . '/', $currentWord); + $splittedWord = preg_split('/' . self::REGEXP_SENTENCE_DELIMITERS . '/', $currentWord) ?: []; foreach ($splittedWord as $currentPart) { if ($currentPart !== '') { $this->iteratorCache->append(new TextIteratorElement($currentPart, $i, Unicode\Functions::strlen($currentPart), false)); @@ -442,7 +446,7 @@ private function parseSubjectBySentence() $count = 0; $delimitersMatches = []; preg_match_all('/' . self::REGEXP_SENTENCE_DELIMITERS . '/', $this->subject, $delimitersMatches); - $splittedSentence = preg_split('/' . self::REGEXP_SENTENCE_DELIMITERS . '/', $this->subject); + $splittedSentence = preg_split('/' . self::REGEXP_SENTENCE_DELIMITERS . '/', $this->subject) ?: []; if (count($splittedSentence) == 1) { $this->iteratorCache->append(new TextIteratorElement($splittedSentence[0], 0, Unicode\Functions::strlen($splittedSentence[0]), false)); @@ -480,6 +484,7 @@ private function parseSubjectBySentence() * Helper function to get the current element from the cache. * * @return TextIteratorElement|null The current element of the cache + * @phpstan-ignore return.unusedType (can be null if next() leaves the valid range of the iterator) */ private function getCurrentElement() { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 9b730bf93b..6eaf9d56c1 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 3 + level: 8 paths: - Neos.Cache/Classes - Neos.Eel/Classes @@ -29,3 +29,6 @@ parameters: - Neos.Eel/Classes/InterpretedEelParser.php - Neos.Eel/Classes/CompilingEelParser.php - Neos.Flow/Classes/Security/Authorization/Privilege/Entity/Doctrine/EntityPrivilegeExpressionEvaluator.php + scanDirectories: + - Neos.Flow/Tests + - Neos.Flow/Classes/Error