diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..3121416e --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,67 @@ +parameters: + ignoreErrors: + - + message: '#^Interface IteratorAggregate specifies template type TKey of interface Traversable as mixed but it''s already specified as \(int\|string\)\.$#' + identifier: generics.interfaceConflict + count: 1 + path: src/FeedContext/ContextList.php + + - + message: '#^Parameter \#1 \$context of method Setono\\SyliusFeedPlugin\\FeedContext\\ContextList\:\:add\(\) expects array\|object, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/FeedContext/ContextList.php + + - + message: '#^PHPDoc tag @var with type Sylius\\Component\\Core\\Model\\ProductTranslationInterface\|null is not subtype of native type Sylius\\Component\\Resource\\Model\\TranslationInterface\|null\.$#' + identifier: varTag.nativeType + count: 1 + path: src/FeedContext/Google/Shopping/ProductItemContext.php + + - + message: '#^PHPDoc tag @var with type Sylius\\Component\\Taxonomy\\Model\\TaxonTranslationInterface\|null is not subtype of native type Sylius\\Component\\Resource\\Model\\TranslationInterface\|null\.$#' + identifier: varTag.nativeType + count: 1 + path: src/FeedContext/Google/Shopping/ProductItemContext.php + + - + message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(Sylius\\Component\\Taxonomy\\Model\\TaxonInterface\)\: mixed\)\|null, Closure\(Sylius\\Component\\Core\\Model\\TaxonInterface\)\: string given\.$#' + identifier: argument.type + count: 1 + path: src/FeedContext/Google/Shopping/ProductItemContext.php + + - + message: '#^Parameter \#1 \$price of method Setono\\SyliusFeedPlugin\\Feed\\Model\\Google\\Shopping\\Product\:\:setPrice\(\) expects Setono\\SyliusFeedPlugin\\Feed\\Model\\Google\\Shopping\\Price\|null, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/FeedContext/Google/Shopping/ProductItemContext.php + + - + message: '#^Parameter \#1 \$salePrice of method Setono\\SyliusFeedPlugin\\Feed\\Model\\Google\\Shopping\\Product\:\:setSalePrice\(\) expects Setono\\SyliusFeedPlugin\\Feed\\Model\\Google\\Shopping\\Price\|null, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/FeedContext/Google/Shopping/ProductItemContext.php + + - + message: '#^Parameter \#1 \$translatable of method Setono\\SyliusFeedPlugin\\FeedContext\\Google\\Shopping\\ProductItemContext\:\:getTranslation\(\) expects Sylius\\Component\\Resource\\Model\\TranslatableInterface, Sylius\\Component\\Core\\Model\\ProductInterface given\.$#' + identifier: argument.type + count: 1 + path: src/FeedContext/Google/Shopping/ProductItemContext.php + + - + message: '#^Parameter \#1 \$translatable of method Setono\\SyliusFeedPlugin\\FeedContext\\Google\\Shopping\\ProductItemContext\:\:getTranslation\(\) expects Sylius\\Component\\Resource\\Model\\TranslatableInterface, Sylius\\Component\\Core\\Model\\TaxonInterface given\.$#' + identifier: argument.type + count: 1 + path: src/FeedContext/Google/Shopping/ProductItemContext.php + + - + message: '#^Parameter \#2 \$currency of class Setono\\SyliusFeedPlugin\\Feed\\Model\\Google\\Shopping\\Price constructor expects string\|Stringable, Sylius\\Component\\Currency\\Model\\CurrencyInterface given\.$#' + identifier: argument.type + count: 1 + path: src/FeedContext/Google/Shopping/ProductItemContext.php + + - + message: '#^PHPDoc tag @var with type array\\|League\\Flysystem\\DirectoryListing\ is not subtype of type League\\Flysystem\\DirectoryListing\\.$#' + identifier: varTag.type + count: 1 + path: src/Message/Handler/FinishGenerationHandler.php diff --git a/phpstan.neon b/phpstan.neon index 947e0c89..8ad23dac 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,6 @@ +includes: + - phpstan-baseline.neon + parameters: level: max @@ -22,7 +25,7 @@ parameters: objectManagerLoader: tests/PHPStan/object_manager.php reportUnmatchedIgnoredErrors: false - treatPhpDocTypesAsCertain: false + treatPhpDocTypesAsCertain: true ignoreErrors: - @@ -31,6 +34,7 @@ parameters: - doctrine.associationType - doctrine.columnType - missingType.generics + - missingType.parameter - identifier: class.notFound message: '#League\\Flysystem\\#' diff --git a/src/Controller/Action/Shop/ShowFeedAction.php b/src/Controller/Action/Shop/ShowFeedAction.php index 68b4dd19..06379277 100644 --- a/src/Controller/Action/Shop/ShowFeedAction.php +++ b/src/Controller/Action/Shop/ShowFeedAction.php @@ -68,9 +68,8 @@ public function __invoke(string $code): StreamedResponse } } - /** @var resource|false $stream */ $stream = $filesystem->readStream((string) $feedPath); - if (false === $stream) { + if (!\is_resource($stream)) { throw new RuntimeException(sprintf('An error occurred trying to read the feed file %s', $feedPath)); } diff --git a/src/DataProvider/DataProvider.php b/src/DataProvider/DataProvider.php index 448c932b..07db829d 100644 --- a/src/DataProvider/DataProvider.php +++ b/src/DataProvider/DataProvider.php @@ -23,6 +23,9 @@ class DataProvider implements DataProviderInterface /** @var CollectionBatcherInterface[] */ private array $batchers = []; + /** + * @param class-string $class + */ public function __construct( private readonly BatcherFactoryInterface $batcherFactory, private readonly QueryRebuilderInterface $queryRebuilder, @@ -53,6 +56,7 @@ public function getBatchCount(ChannelInterface $channel, LocaleInterface $locale public function getItems(BatchInterface $batch): iterable { + /** @phpstan-ignore return.type */ return $this->queryRebuilder->rebuild($batch)->getResult(); } diff --git a/src/DataProvider/DataProviderInterface.php b/src/DataProvider/DataProviderInterface.php index 51c04cc9..b91d97f4 100644 --- a/src/DataProvider/DataProviderInterface.php +++ b/src/DataProvider/DataProviderInterface.php @@ -14,6 +14,8 @@ interface DataProviderInterface { /** * This will be the root class of the data provided by this data provider + * + * @return class-string */ public function getClass(): string; diff --git a/src/DependencyInjection/Compiler/RegisterFilesystemPass.php b/src/DependencyInjection/Compiler/RegisterFilesystemPass.php index 64d28447..bd9d471c 100644 --- a/src/DependencyInjection/Compiler/RegisterFilesystemPass.php +++ b/src/DependencyInjection/Compiler/RegisterFilesystemPass.php @@ -33,6 +33,8 @@ public function process(ContainerBuilder $container): void foreach (self::PARAMETERS as $parameter) { $parameterValue = $container->getParameter($parameter); + Assert::string($parameterValue); + if (!$container->hasDefinition($parameterValue)) { throw new InvalidArgumentException(sprintf('No service definition exists with id "%s"', $parameterValue)); } diff --git a/src/DependencyInjection/Compiler/ValidateDataProvidersPass.php b/src/DependencyInjection/Compiler/ValidateDataProvidersPass.php index 85e4b80a..125e1281 100644 --- a/src/DependencyInjection/Compiler/ValidateDataProvidersPass.php +++ b/src/DependencyInjection/Compiler/ValidateDataProvidersPass.php @@ -23,6 +23,7 @@ public function process(ContainerBuilder $container): void throw new InvalidArgumentException(sprintf('The service %s needs the code attribute. Something like this: ', $id)); } + /** @var array{code?: string} $attributes */ foreach ($tagged as $attributes) { if (!isset($attributes['code'])) { throw new InvalidArgumentException(sprintf('The service %s needs the code attribute. Something like this: ', $id)); diff --git a/src/DependencyInjection/SetonoSyliusFeedExtension.php b/src/DependencyInjection/SetonoSyliusFeedExtension.php index 80cfd57a..20c927c1 100644 --- a/src/DependencyInjection/SetonoSyliusFeedExtension.php +++ b/src/DependencyInjection/SetonoSyliusFeedExtension.php @@ -18,6 +18,13 @@ final class SetonoSyliusFeedExtension extends AbstractResourceExtension implemen { public function load(array $configs, ContainerBuilder $container): void { + /** + * @var array{ + * driver: string, + * storage: array{feed: string, feed_tmp: string}, + * resources: array + * } $config + */ $config = $this->processConfiguration($this->getConfiguration([], $container), $configs); $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); diff --git a/src/Doctrine/ORM/FeedRepository.php b/src/Doctrine/ORM/FeedRepository.php index f862abca..3021aa66 100644 --- a/src/Doctrine/ORM/FeedRepository.php +++ b/src/Doctrine/ORM/FeedRepository.php @@ -13,12 +13,16 @@ class FeedRepository extends EntityRepository implements FeedRepositoryInterface { public function findOneByCode(string $code): ?FeedInterface { - return $this->createQueryBuilder('o') + $result = $this->createQueryBuilder('o') ->where('o.code = :code') ->setParameter('code', $code) ->getQuery() ->getOneOrNullResult() ; + + Assert::nullOrIsInstanceOf($result, FeedInterface::class); + + return $result; } public function findEnabled(): array @@ -29,6 +33,7 @@ public function findEnabled(): array ->getResult() ; + Assert::isArray($res); Assert::allIsInstanceOf($res, FeedInterface::class); return $res; diff --git a/src/Doctrine/ORM/ViolationRepository.php b/src/Doctrine/ORM/ViolationRepository.php index 9df97613..efa2a26a 100644 --- a/src/Doctrine/ORM/ViolationRepository.php +++ b/src/Doctrine/ORM/ViolationRepository.php @@ -19,8 +19,6 @@ public function findCountsGroupedBySeverity($feed = null): array $feed = (int) $feed->getId(); } - Assert::nullOrInteger($feed); - $qb = $this->createQueryBuilder('o') ->select('NEW Setono\SyliusFeedPlugin\DTO\SeverityCount(o.severity, count(o))') ->groupBy('o.severity') @@ -32,6 +30,7 @@ public function findCountsGroupedBySeverity($feed = null): array } $res = $qb->getQuery()->getResult(); + Assert::isArray($res); Assert::allIsInstanceOf($res, SeverityCount::class); return $res; diff --git a/src/EventListener/Filter/AbstractFilterListener.php b/src/EventListener/Filter/AbstractFilterListener.php index 177ccdd9..6c114a6b 100644 --- a/src/EventListener/Filter/AbstractFilterListener.php +++ b/src/EventListener/Filter/AbstractFilterListener.php @@ -15,6 +15,9 @@ public function __construct(private readonly string $class) { } + /** + * @param list $additionalInstanceChecks + */ protected function isEligible(QueryBuilderEvent $event, array $additionalInstanceChecks = []): bool { $class = $event->getDataProvider()->getClass(); @@ -39,7 +42,7 @@ protected function getAlias(QueryBuilder $queryBuilder): string throw new InvalidArgumentException('This filter only works with one root alias'); } - return reset($aliases); + return $aliases[0]; } protected function getClassMetadata(QueryBuilderEvent $event): ClassMetadata diff --git a/src/EventListener/MoveGeneratedFeedSubscriber.php b/src/EventListener/MoveGeneratedFeedSubscriber.php index 46fd1b66..5c9411f6 100644 --- a/src/EventListener/MoveGeneratedFeedSubscriber.php +++ b/src/EventListener/MoveGeneratedFeedSubscriber.php @@ -87,9 +87,8 @@ public function move(TransitionEvent $event): void ); $temporaryFilesystem = $this->temporaryFilesystem; $temporaryPath = TemporaryFeedPathGenerator::getBaseFile($temporaryDir); - /** @var resource|false $tempFile */ $tempFile = $temporaryFilesystem->readStream((string) $temporaryPath); - if (false === $tempFile) { + if (!\is_resource($tempFile)) { throw new \RuntimeException(sprintf( 'The file with path "%s" could not be found', $temporaryPath, diff --git a/src/Exception/GenerateBatchException.php b/src/Exception/GenerateBatchException.php index 926dae51..347655d9 100644 --- a/src/Exception/GenerateBatchException.php +++ b/src/Exception/GenerateBatchException.php @@ -14,8 +14,8 @@ final class GenerateBatchException extends RuntimeException implements Exception private ?int $feedId = null; - /** @var mixed */ - private $resourceId; + /** @var scalar|null */ + private mixed $resourceId = null; private ?string $channelCode = null; @@ -40,18 +40,12 @@ public function setFeedId(int $feedId): void $this->updateMessage(); } - /** - * @return mixed - */ - public function getResourceId() + public function getResourceId(): mixed { return $this->resourceId; } - /** - * @param mixed $resourceId - */ - public function setResourceId($resourceId): void + public function setResourceId(mixed $resourceId): void { Assert::scalar($resourceId); diff --git a/src/Factory/ViolationFactory.php b/src/Factory/ViolationFactory.php index 600079f4..4da7d1ee 100644 --- a/src/Factory/ViolationFactory.php +++ b/src/Factory/ViolationFactory.php @@ -39,13 +39,14 @@ public function createFromConstraintViolation( $violation->setChannel($channel); $violation->setLocale($locale); $violation->setMessage( - $constraintViolation->getPropertyPath() . ': ' . sprintf((string) $constraintViolation->getMessage(), $constraintViolation->getInvalidValue()), + /** @phpstan-ignore cast.string */ + $constraintViolation->getPropertyPath() . ': ' . sprintf((string) $constraintViolation->getMessage(), (string) $constraintViolation->getInvalidValue()), ); $violation->setData($data); if ($constraintViolation instanceof ConstraintViolation) { $constraint = $constraintViolation->getConstraint(); - if (null !== $constraint && isset($constraint->payload['severity'])) { + if (null !== $constraint && is_array($constraint->payload) && isset($constraint->payload['severity']) && is_string($constraint->payload['severity'])) { $violation->setSeverity($constraint->payload['severity']); } } diff --git a/src/Feed/Model/Google/Shopping/Price.php b/src/Feed/Model/Google/Shopping/Price.php index 979edefa..25beb361 100644 --- a/src/Feed/Model/Google/Shopping/Price.php +++ b/src/Feed/Model/Google/Shopping/Price.php @@ -14,7 +14,7 @@ final class Price implements JsonSerializable, \Stringable private readonly string $currency; /** - * @param object|string $currency + * @param \Stringable|string $currency */ public function __construct(int $amount, $currency) { diff --git a/src/FeedContext/ContextListInterface.php b/src/FeedContext/ContextListInterface.php index 801a0a93..86e6fda3 100644 --- a/src/FeedContext/ContextListInterface.php +++ b/src/FeedContext/ContextListInterface.php @@ -8,7 +8,7 @@ use Traversable; /** - * @extends Traversable + * @extends Traversable */ interface ContextListInterface extends Traversable, Countable { diff --git a/src/FeedType/FeedType.php b/src/FeedType/FeedType.php index 4d8db8b7..3a9470e8 100644 --- a/src/FeedType/FeedType.php +++ b/src/FeedType/FeedType.php @@ -11,8 +11,12 @@ final class FeedType implements FeedTypeInterface { + /** @var list */ private readonly array $validationGroups; + /** + * @param list $validationGroups + */ public function __construct( private readonly string $code, private readonly string $template, diff --git a/src/FeedType/FeedTypeInterface.php b/src/FeedType/FeedTypeInterface.php index d5f013f1..79ecb065 100644 --- a/src/FeedType/FeedTypeInterface.php +++ b/src/FeedType/FeedTypeInterface.php @@ -24,6 +24,8 @@ public function getItemContext(): ItemContextInterface; /** * The validation groups used when validating each item + * + * @return list */ public function getValidationGroups(): array; } diff --git a/src/Message/Handler/FinishGenerationHandler.php b/src/Message/Handler/FinishGenerationHandler.php index ca20c972..b347ef66 100644 --- a/src/Message/Handler/FinishGenerationHandler.php +++ b/src/Message/Handler/FinishGenerationHandler.php @@ -88,7 +88,7 @@ public function __invoke(FinishGeneration $message): void [$feedStart, $feedEnd] = $this->getFeedParts($feed, $feedType, $channel, $locale); - fwrite($batchStream, (string) $feedStart); + fwrite($batchStream, $feedStart); $filesystem = $this->filesystem; @@ -105,9 +105,8 @@ public function __invoke(FinishGeneration $message): void } /** @var string $path */ $path = $file['path']; - /** @var resource|false $fp */ $fp = $filesystem->readStream($path); - if (false === $fp) { + if (!is_resource($fp)) { throw new \RuntimeException(sprintf( 'The file "%s" could not be opened as a resource', $path, @@ -115,7 +114,7 @@ public function __invoke(FinishGeneration $message): void } while (!feof($fp)) { - fwrite($batchStream, fread($fp, 8192)); + fwrite($batchStream, (string) fread($fp, 8192)); } fclose($fp); @@ -123,7 +122,7 @@ public function __invoke(FinishGeneration $message): void $filesystem->delete($path); } - fwrite($batchStream, (string) $feedEnd); + fwrite($batchStream, $feedEnd); if (interface_exists(FilesystemInterface::class) && $filesystem instanceof FilesystemInterface) { /** @var resource|false $res */ @@ -170,9 +169,18 @@ public function __invoke(FinishGeneration $message): void private function getBatchStream() { // needs to be w+ since we use the same stream later to read from - return fopen('php://temp', 'w+b'); + $resource = fopen('php://temp', 'w+b'); + + if (!is_resource($resource)) { + throw new RuntimeException('Could not open the stream'); + } + + return $resource; } + /** + * @return non-empty-list + */ private function getFeedParts( FeedInterface $feed, FeedTypeInterface $feedType, diff --git a/src/Message/Handler/GenerateBatchHandler.php b/src/Message/Handler/GenerateBatchHandler.php index fe59babd..3275e3d1 100644 --- a/src/Message/Handler/GenerateBatchHandler.php +++ b/src/Message/Handler/GenerateBatchHandler.php @@ -276,7 +276,13 @@ private function getWorkflow(FeedInterface $feed): WorkflowInterface private function openStream() { // needs to be w+ since we use the same stream later to read from - return fopen('php://temp', 'w+b'); + $resource = fopen('php://temp', 'w+b'); + + if (!is_resource($resource)) { + throw new \RuntimeException('Could not open the stream'); + } + + return $resource; } private function applyErrorTransition(WorkflowInterface $workflow, FeedInterface $feed): void diff --git a/src/Model/BrandAwareInterface.php b/src/Model/BrandAwareInterface.php index e1a1c567..f523062c 100644 --- a/src/Model/BrandAwareInterface.php +++ b/src/Model/BrandAwareInterface.php @@ -13,7 +13,7 @@ interface BrandAwareInterface /** * Must return a string or an object with __toString implemented * - * @return string|object|null + * @return string|\Stringable|null */ public function getBrand(); } diff --git a/src/Model/ColorAwareInterface.php b/src/Model/ColorAwareInterface.php index 485f509e..47e74209 100644 --- a/src/Model/ColorAwareInterface.php +++ b/src/Model/ColorAwareInterface.php @@ -13,7 +13,7 @@ interface ColorAwareInterface /** * Must return a string or an object with __toString implemented * - * @return string|object|null + * @return string|\Stringable|null */ public function getColor(); } diff --git a/src/Model/ConditionAwareInterface.php b/src/Model/ConditionAwareInterface.php index 4a1fc3e7..de5c47e7 100644 --- a/src/Model/ConditionAwareInterface.php +++ b/src/Model/ConditionAwareInterface.php @@ -9,7 +9,7 @@ interface ConditionAwareInterface /** * Must return a string or an object with __toString implemented * - * @return string|object|null + * @return string|\Stringable|null */ public function getCondition(); } diff --git a/src/Model/GtinAwareInterface.php b/src/Model/GtinAwareInterface.php index 5c0e888a..456032ad 100644 --- a/src/Model/GtinAwareInterface.php +++ b/src/Model/GtinAwareInterface.php @@ -9,7 +9,7 @@ interface GtinAwareInterface /** * Must return a string or an object with __toString implemented * - * @return string|object|null + * @return string|\Stringable|null */ public function getGtin(); } diff --git a/src/Model/LocalizedBrandAwareInterface.php b/src/Model/LocalizedBrandAwareInterface.php index 1bdbb643..58bb2007 100644 --- a/src/Model/LocalizedBrandAwareInterface.php +++ b/src/Model/LocalizedBrandAwareInterface.php @@ -11,7 +11,7 @@ interface LocalizedBrandAwareInterface /** * Must return a string or an object with __toString implemented * - * @return string|object|null + * @return string|\Stringable|null */ public function getBrand(LocaleInterface $locale); } diff --git a/src/Model/LocalizedColorAwareInterface.php b/src/Model/LocalizedColorAwareInterface.php index cc2d414c..3deddb6d 100644 --- a/src/Model/LocalizedColorAwareInterface.php +++ b/src/Model/LocalizedColorAwareInterface.php @@ -11,7 +11,7 @@ interface LocalizedColorAwareInterface /** * Must return a string or an object with __toString implemented * - * @return string|object|null + * @return string|\Stringable|null */ public function getColor(LocaleInterface $locale); } diff --git a/src/Model/LocalizedSizeAwareInterface.php b/src/Model/LocalizedSizeAwareInterface.php index 2b883a3b..0a4060bb 100644 --- a/src/Model/LocalizedSizeAwareInterface.php +++ b/src/Model/LocalizedSizeAwareInterface.php @@ -11,7 +11,7 @@ interface LocalizedSizeAwareInterface /** * Must return a string or an object with __toString implemented * - * @return string|object|null + * @return string|\Stringable|null */ public function getSize(LocaleInterface $locale); } diff --git a/src/Model/MpnAwareInterface.php b/src/Model/MpnAwareInterface.php index a2b63f34..f6ce6a92 100644 --- a/src/Model/MpnAwareInterface.php +++ b/src/Model/MpnAwareInterface.php @@ -9,7 +9,7 @@ interface MpnAwareInterface /** * Must return a string or an object with __toString implemented * - * @return string|object|null + * @return string|\Stringable|null */ public function getMpn(); } diff --git a/src/Model/SizeAwareInterface.php b/src/Model/SizeAwareInterface.php index 46f07b53..cf591981 100644 --- a/src/Model/SizeAwareInterface.php +++ b/src/Model/SizeAwareInterface.php @@ -13,7 +13,7 @@ interface SizeAwareInterface /** * Must return a string or an object with __toString implemented * - * @return string|object|null + * @return string|\Stringable|null */ public function getSize(); } diff --git a/src/Repository/FeedRepositoryInterface.php b/src/Repository/FeedRepositoryInterface.php index 52fd4698..99b0e4d3 100644 --- a/src/Repository/FeedRepositoryInterface.php +++ b/src/Repository/FeedRepositoryInterface.php @@ -17,7 +17,7 @@ public function findOneByCode(string $code): ?FeedInterface; /** * Returns all enabled feeds * - * @return FeedInterface[] + * @return array */ public function findEnabled(): array; diff --git a/src/Twig/Extension.php b/src/Twig/Extension.php index 0c1e811b..5f1600b4 100644 --- a/src/Twig/Extension.php +++ b/src/Twig/Extension.php @@ -37,7 +37,7 @@ public function getFunctions(): array public function removeEmptyTags(string $xml): string { - return preg_replace('#<[^/>][^>]*>]+>#', '', $xml); + return (string) preg_replace('#<[^/>][^>]*>]+>#', '', $xml); } public function generateFeedUrl(FeedInterface $feed, ChannelInterface $channel, LocaleInterface $locale): string