Merge pull request #1122 from acelaya-forks/feature/phpstan-level

Feature/phpstan level
This commit is contained in:
Alejandro Celaya 2021-07-20 14:01:45 +02:00 committed by GitHub
commit 32bb66c42b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 131 additions and 77 deletions

View file

@ -18,7 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
This behavior needs to be actively opted in, via installer config options or env vars. This behavior needs to be actively opted in, via installer config options or env vars.
### Changed ### Changed
* *Nothing* * [#1118](https://github.com/shlinkio/shlink/issues/1118) Increased phpstan required level to 8.
### Deprecated ### Deprecated
* *Nothing* * *Nothing*

View file

@ -3,5 +3,8 @@
declare(strict_types=1); declare(strict_types=1);
$run = require __DIR__ . '/../config/run.php'; use Symfony\Component\Console\Application;
$run(true);
/** @var Application $app */
$app = require __DIR__ . '/../config/cli-app.php';
$app->run();

View file

@ -62,18 +62,20 @@
}, },
"require-dev": { "require-dev": {
"devster/ubench": "^2.1", "devster/ubench": "^2.1",
"dms/phpunit-arraysubset-asserts": "^0.2.1", "dms/phpunit-arraysubset-asserts": "^0.3.0",
"eaglewu/swoole-ide-helper": "dev-master", "eaglewu/swoole-ide-helper": "dev-master",
"infection/infection": "^0.21.0", "infection/infection": "^0.23.0",
"phpspec/prophecy-phpunit": "^2.0", "phpspec/prophecy-phpunit": "^2.0",
"phpstan/phpstan": "^0.12.64", "phpstan/phpstan": "^0.12.93",
"phpstan/phpstan-doctrine": "^0.12.42",
"phpstan/phpstan-symfony": "^0.12.41",
"phpunit/php-code-coverage": "^9.2", "phpunit/php-code-coverage": "^9.2",
"phpunit/phpunit": "^9.5", "phpunit/phpunit": "^9.5",
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~2.1.1", "shlinkio/php-coding-standard": "~2.1.1",
"shlinkio/shlink-test-utils": "^2.1", "shlinkio/shlink-test-utils": "^2.1",
"symfony/var-dumper": "^5.2", "symfony/var-dumper": "^5.2",
"veewee/composer-run-parallel": "^0.1.0" "veewee/composer-run-parallel": "^1.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -112,7 +114,7 @@
], ],
"cs": "phpcs", "cs": "phpcs",
"cs:fix": "phpcbf", "cs:fix": "phpcbf",
"stan": "phpstan analyse module/*/src/ module/*/config config docker/config data/migrations --level=6", "stan": "APP_ENV=test php vendor/bin/phpstan analyse module/*/src module/*/config config docker/config data/migrations --level=8",
"test": [ "test": [
"@test:unit", "@test:unit",
"@test:db", "@test:db",

12
config/cli-app.php Normal file
View file

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Application as CliApp;
return (static function () {
/** @var ContainerInterface $container */
$container = include __DIR__ . '/container.php';
return $container->get(CliApp::class);
})();

View file

@ -4,12 +4,9 @@ declare(strict_types=1);
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Console\ConsoleRunner; use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Psr\Container\ContainerInterface;
return (function () {
/** @var ContainerInterface $container */
$container = include __DIR__ . '/container.php';
$em = $container->get(EntityManager::class);
return (static function () {
/** @var EntityManager $em */
$em = include __DIR__ . '/entity-manager.php';
return ConsoleRunner::createHelperSet($em); return ConsoleRunner::createHelperSet($em);
})(); })();

12
config/entity-manager.php Normal file
View file

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
use Doctrine\ORM\EntityManager;
use Psr\Container\ContainerInterface;
return (static function () {
/** @var ContainerInterface $container */
$container = include __DIR__ . '/container.php';
return $container->get(EntityManager::class);
})();

View file

@ -4,12 +4,11 @@ declare(strict_types=1);
use Mezzio\Application; use Mezzio\Application;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Application as CliApp;
return function (bool $isCli = false): void { return static function (): void {
/** @var ContainerInterface $container */ /** @var ContainerInterface $container */
$container = include __DIR__ . '/container.php'; $container = include __DIR__ . '/container.php';
$app = $container->get($isCli ? CliApp::class : Application::class); $app = $container->get(Application::class);
$app->run(); $app->run();
}; };

View file

@ -120,7 +120,7 @@ return [
'name' => 'start_collecting_coverage', 'name' => 'start_collecting_coverage',
'path' => '/api-tests/start-coverage', 'path' => '/api-tests/start-coverage',
'middleware' => middleware(static function () use (&$coverage) { 'middleware' => middleware(static function () use (&$coverage) {
if ($coverage) { if ($coverage) { // @phpstan-ignore-line
$coverage->start('API tests'); $coverage->start('API tests');
} }
return new EmptyResponse(); return new EmptyResponse();
@ -131,7 +131,7 @@ return [
'name' => 'dump_coverage', 'name' => 'dump_coverage',
'path' => '/api-tests/stop-coverage', 'path' => '/api-tests/stop-coverage',
'middleware' => middleware(static function () use (&$coverage) { 'middleware' => middleware(static function () use (&$coverage) {
if ($coverage) { if ($coverage) { // @phpstan-ignore-line
$basePath = __DIR__ . '/../../build/coverage-api'; $basePath = __DIR__ . '/../../build/coverage-api';
$coverage->stop(); $coverage->stop();
(new PHP())->process($coverage, $basePath . '.cov'); (new PHP())->process($coverage, $basePath . '.cov');

View file

@ -8,6 +8,8 @@ use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition; use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use function is_string;
class RoleResolver implements RoleResolverInterface class RoleResolver implements RoleResolverInterface
{ {
public function __construct(private DomainServiceInterface $domainService) public function __construct(private DomainServiceInterface $domainService)
@ -23,7 +25,7 @@ class RoleResolver implements RoleResolverInterface
if ($author) { if ($author) {
$roleDefinitions[] = RoleDefinition::forAuthoredShortUrls(); $roleDefinitions[] = RoleDefinition::forAuthoredShortUrls();
} }
if ($domainAuthority !== null) { if (is_string($domainAuthority)) {
$domain = $this->domainService->getOrCreate($domainAuthority); $domain = $this->domainService->getOrCreate($domainAuthority);
$roleDefinitions[] = RoleDefinition::forDomain($domain); $roleDefinitions[] = RoleDefinition::forDomain($domain);
} }

View file

@ -12,40 +12,36 @@ use function Shlinkio\Shlink\Core\kebabCaseToCamelCase;
use function sprintf; use function sprintf;
use function str_contains; use function str_contains;
/** @deprecated */
abstract class BaseCommand extends Command abstract class BaseCommand extends Command
{ {
/** /**
* @param mixed|null $default * @param string|string[]|bool|null $default
*/ */
protected function addOptionWithDeprecatedFallback( protected function addOptionWithDeprecatedFallback(
string $name, string $name,
?string $shortcut = null, ?string $shortcut = null,
?int $mode = null, ?int $mode = null,
string $description = '', string $description = '',
$default = null, bool|string|array|null $default = null,
): self { ): self {
$this->addOption($name, $shortcut, $mode, $description, $default); $this->addOption($name, $shortcut, $mode, $description, $default);
if (str_contains($name, '-')) { if (str_contains($name, '-')) {
$camelCaseName = kebabCaseToCamelCase($name); $camelCaseName = kebabCaseToCamelCase($name);
$this->addOption($camelCaseName, null, $mode, sprintf('[DEPRECATED] Same as "%s".', $name), $default); $this->addOption($camelCaseName, null, $mode, sprintf('[DEPRECATED] Alias for "%s".', $name), $default);
} }
return $this; return $this;
} }
/** // @phpstan-ignore-next-line
* @return bool|string|string[]|null protected function getOptionWithDeprecatedFallback(InputInterface $input, string $name) // phpcs:ignore
*/
protected function getOptionWithDeprecatedFallback(InputInterface $input, string $name)
{ {
$rawInput = method_exists($input, '__toString') ? $input->__toString() : ''; $rawInput = method_exists($input, '__toString') ? $input->__toString() : '';
$camelCaseName = kebabCaseToCamelCase($name); $camelCaseName = kebabCaseToCamelCase($name);
$resolvedOptionName = str_contains($rawInput, $camelCaseName) ? $camelCaseName : $name;
if (str_contains($rawInput, $camelCaseName)) { return $input->getOption($resolvedOptionName);
return $input->getOption($camelCaseName);
}
return $input->getOption($name);
} }
} }

View file

@ -32,6 +32,6 @@ abstract class AbstractDatabaseCommand extends AbstractLockedCommand
protected function getLockConfig(): LockedCommandConfig protected function getLockConfig(): LockedCommandConfig
{ {
return LockedCommandConfig::blocking($this->getName()); return LockedCommandConfig::blocking($this->getName() ?? static::class);
} }
} }

View file

@ -11,6 +11,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Throwable; use Throwable;
use function is_string;
use function sprintf; use function sprintf;
abstract class AbstractWithDateRangeCommand extends BaseCommand abstract class AbstractWithDateRangeCommand extends BaseCommand
@ -49,7 +50,7 @@ abstract class AbstractWithDateRangeCommand extends BaseCommand
private function getDateOption(InputInterface $input, OutputInterface $output, string $key): ?Chronos private function getDateOption(InputInterface $input, OutputInterface $output, string $key): ?Chronos
{ {
$value = $this->getOptionWithDeprecatedFallback($input, $key); $value = $this->getOptionWithDeprecatedFallback($input, $key);
if (empty($value)) { if (empty($value) || ! is_string($value)) {
return null; return null;
} }

View file

@ -45,8 +45,8 @@ class DownloadGeoLiteDbCommand extends Command
$io->text(sprintf('<fg=blue>%s GeoLite2 db file...</>', $olderDbExists ? 'Updating' : 'Downloading')); $io->text(sprintf('<fg=blue>%s GeoLite2 db file...</>', $olderDbExists ? 'Updating' : 'Downloading'));
$this->progressBar = new ProgressBar($io); $this->progressBar = new ProgressBar($io);
}, function (int $total, int $downloaded): void { }, function (int $total, int $downloaded): void {
$this->progressBar->setMaxSteps($total); $this->progressBar?->setMaxSteps($total);
$this->progressBar->setProgress($downloaded); $this->progressBar?->setProgress($downloaded);
}); });
if ($this->progressBar === null) { if ($this->progressBar === null) {

View file

@ -139,7 +139,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
throw IpCannotBeLocatedException::forEmptyAddress(); throw IpCannotBeLocatedException::forEmptyAddress();
} }
$ipAddr = $visit->getRemoteAddr(); $ipAddr = $visit->getRemoteAddr() ?? '';
$this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr)); $this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
if ($ipAddr === IpAddress::LOCALHOST) { if ($ipAddr === IpAddress::LOCALHOST) {
$this->io->writeln(' [<comment>Ignored localhost address</comment>]'); $this->io->writeln(' [<comment>Ignored localhost address</comment>]');
@ -168,7 +168,12 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
private function checkDbUpdate(InputInterface $input): void private function checkDbUpdate(InputInterface $input): void
{ {
$downloadDbCommand = $this->getApplication()->find(DownloadGeoLiteDbCommand::NAME); $cliApp = $this->getApplication();
if ($cliApp === null) {
return;
}
$downloadDbCommand = $cliApp->find(DownloadGeoLiteDbCommand::NAME);
$exitCode = $downloadDbCommand->run($input, $this->io); $exitCode = $downloadDbCommand->run($input, $this->io);
if ($exitCode === ExitCodes::EXIT_FAILURE) { if ($exitCode === ExitCodes::EXIT_FAILURE) {
@ -178,6 +183,6 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
protected function getLockConfig(): LockedCommandConfig protected function getLockConfig(): LockedCommandConfig
{ {
return LockedCommandConfig::nonBlocking($this->getName()); return LockedCommandConfig::nonBlocking(self::NAME);
} }
} }

View file

@ -42,10 +42,7 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
return $e; return $e;
} }
/** public static function withInvalidEpochInOldDb(mixed $buildEpoch): self
* @param mixed $buildEpoch
*/
public static function withInvalidEpochInOldDb($buildEpoch): self
{ {
$e = new self(sprintf( $e = new self(sprintf(
'Build epoch with value "%s" from existing geolocation database, could not be parsed to integer.', 'Build epoch with value "%s" from existing geolocation database, could not be parsed to integer.',

View file

@ -68,6 +68,21 @@ class RoleResolverTest extends TestCase
[RoleDefinition::forDomain($domain)], [RoleDefinition::forDomain($domain)],
1, 1,
]; ];
yield 'false domain role' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => false]),
[],
0,
];
yield 'true domain role' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => true]),
[],
0,
];
yield 'string array domain role' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => ['foo', 'bar']]),
[],
0,
];
yield 'author role only' => [ yield 'author role only' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => null, RoleResolver::AUTHOR_ONLY_PARAM => true]), $buildInput([RoleResolver::DOMAIN_ONLY_PARAM => null, RoleResolver::AUTHOR_ONLY_PARAM => true]),
[RoleDefinition::forAuthoredShortUrls()], [RoleDefinition::forAuthoredShortUrls()],

View file

@ -23,6 +23,7 @@ class RobotsAction implements RequestHandlerInterface, StatusCodeInterface
public function handle(ServerRequestInterface $request): ResponseInterface public function handle(ServerRequestInterface $request): ResponseInterface
{ {
// @phpstan-ignore-next-line The "Response" phpdoc is wrong
return new Response(self::STATUS_OK, ['Content-type' => 'text/plain'], $this->buildRobots()); return new Response(self::STATUS_OK, ['Content-type' => 'text/plain'], $this->buildRobots());
} }

View file

@ -57,7 +57,6 @@ class DomainService implements DomainServiceInterface
public function getOrCreate(string $authority): Domain public function getOrCreate(string $authority): Domain
{ {
$repo = $this->em->getRepository(Domain::class); $repo = $this->em->getRepository(Domain::class);
/** @var Domain|null $domain */
$domain = $repo->findOneBy(['authority' => $authority]) ?? new Domain($authority); $domain = $repo->findOneBy(['authority' => $authority]) ?? new Domain($authority);
$this->em->persist($domain); $this->em->persist($domain);

View file

@ -26,17 +26,20 @@ class NotFoundRedirectHandler implements MiddlewareInterface
$notFoundType = $request->getAttribute(NotFoundType::class); $notFoundType = $request->getAttribute(NotFoundType::class);
if ($notFoundType->isBaseUrl() && $this->redirectOptions->hasBaseUrlRedirect()) { if ($notFoundType->isBaseUrl() && $this->redirectOptions->hasBaseUrlRedirect()) {
// @phpstan-ignore-next-line Create custom PHPStan rule
return $this->redirectResponseHelper->buildRedirectResponse($this->redirectOptions->getBaseUrlRedirect()); return $this->redirectResponseHelper->buildRedirectResponse($this->redirectOptions->getBaseUrlRedirect());
} }
if ($notFoundType->isRegularNotFound() && $this->redirectOptions->hasRegular404Redirect()) { if ($notFoundType->isRegularNotFound() && $this->redirectOptions->hasRegular404Redirect()) {
return $this->redirectResponseHelper->buildRedirectResponse( return $this->redirectResponseHelper->buildRedirectResponse(
// @phpstan-ignore-next-line Create custom PHPStan rule
$this->redirectOptions->getRegular404Redirect(), $this->redirectOptions->getRegular404Redirect(),
); );
} }
if ($notFoundType->isInvalidShortUrl() && $this->redirectOptions->hasInvalidShortUrlRedirect()) { if ($notFoundType->isInvalidShortUrl() && $this->redirectOptions->hasInvalidShortUrlRedirect()) {
return $this->redirectResponseHelper->buildRedirectResponse( return $this->redirectResponseHelper->buildRedirectResponse(
// @phpstan-ignore-next-line Create custom PHPStan rule
$this->redirectOptions->getInvalidShortUrlRedirect(), $this->redirectOptions->getInvalidShortUrlRedirect(),
); );
} }

View file

@ -55,7 +55,7 @@ class LocateVisit
} }
$isLocatable = $originalIpAddress !== null || $visit->isLocatable(); $isLocatable = $originalIpAddress !== null || $visit->isLocatable();
$addr = $originalIpAddress ?? $visit->getRemoteAddr(); $addr = $originalIpAddress ?? $visit->getRemoteAddr() ?? '';
try { try {
$location = $isLocatable ? $this->ipLocationResolver->resolveIpLocation($addr) : Location::emptyInstance(); $location = $isLocatable ? $this->ipLocationResolver->resolveIpLocation($addr) : Location::emptyInstance();

View file

@ -28,7 +28,7 @@ class ImportedLinksProcessor implements ImportedLinksProcessorInterface
private ShortCodeHelperInterface $shortCodeHelper, private ShortCodeHelperInterface $shortCodeHelper,
private DoctrineBatchHelperInterface $batchHelper private DoctrineBatchHelperInterface $batchHelper
) { ) {
$this->shortUrlRepo = $this->em->getRepository(ShortUrl::class); // @phpstan-ignore-line $this->shortUrlRepo = $this->em->getRepository(ShortUrl::class);
} }
/** /**

View file

@ -42,7 +42,7 @@ final class MercureUpdatesGenerator implements MercureUpdatesGeneratorInterface
public function newShortUrlVisitUpdate(Visit $visit): Update public function newShortUrlVisitUpdate(Visit $visit): Update
{ {
$shortUrl = $visit->getShortUrl(); $shortUrl = $visit->getShortUrl();
$topic = sprintf('%s/%s', self::NEW_VISIT_TOPIC, $shortUrl->getShortCode()); $topic = sprintf('%s/%s', self::NEW_VISIT_TOPIC, $shortUrl?->getShortCode());
return new Update($topic, $this->serialize([ return new Update($topic, $this->serialize([
'shortUrl' => $this->shortUrlTransformer->transform($shortUrl), 'shortUrl' => $this->shortUrlTransformer->transform($shortUrl),

View file

@ -49,7 +49,6 @@ final class ShortUrlsOrdering
]); ]);
} }
/** @var string|array $orderBy */
if (! $isArray) { if (! $isArray) {
[$field, $dir] = array_pad(explode('-', $orderBy), 2, null); [$field, $dir] = array_pad(explode('-', $orderBy), 2, null);
$this->orderField = $field; $this->orderField = $field;

View file

@ -19,7 +19,7 @@ final class ShortUrlsParams
private array $tags; private array $tags;
private ShortUrlsOrdering $orderBy; private ShortUrlsOrdering $orderBy;
private ?DateRange $dateRange; private ?DateRange $dateRange;
private ?int $itemsPerPage = null; private int $itemsPerPage;
private function __construct() private function __construct()
{ {

View file

@ -29,13 +29,16 @@ final class Visitor
$this->userAgent = $this->cropToLength($userAgent, self::USER_AGENT_MAX_LENGTH); $this->userAgent = $this->cropToLength($userAgent, self::USER_AGENT_MAX_LENGTH);
$this->referer = $this->cropToLength($referer, self::REFERER_MAX_LENGTH); $this->referer = $this->cropToLength($referer, self::REFERER_MAX_LENGTH);
$this->visitedUrl = $this->cropToLength($visitedUrl, self::VISITED_URL_MAX_LENGTH); $this->visitedUrl = $this->cropToLength($visitedUrl, self::VISITED_URL_MAX_LENGTH);
$this->remoteAddress = $this->cropToLength($remoteAddress, self::REMOTE_ADDRESS_MAX_LENGTH); $this->remoteAddress = $remoteAddress === null ? null : $this->cropToLength(
$remoteAddress,
self::REMOTE_ADDRESS_MAX_LENGTH,
);
$this->potentialBot = isCrawler($userAgent); $this->potentialBot = isCrawler($userAgent);
} }
private function cropToLength(?string $value, int $length): ?string private function cropToLength(string $value, int $length): string
{ {
return $value === null ? null : substr($value, 0, $length); return substr($value, 0, $length);
} }
public static function fromRequest(ServerRequestInterface $request): self public static function fromRequest(ServerRequestInterface $request): self

View file

@ -13,7 +13,7 @@ final class VisitsParams
private const FIRST_PAGE = 1; private const FIRST_PAGE = 1;
private const ALL_ITEMS = -1; private const ALL_ITEMS = -1;
private ?DateRange $dateRange; private DateRange $dateRange;
private int $itemsPerPage; private int $itemsPerPage;
public function __construct( public function __construct(

View file

@ -18,7 +18,6 @@ use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl; use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
use function array_column; use function array_column;
use function array_key_exists;
use function count; use function count;
use function Functional\contains; use function Functional\contains;
@ -59,6 +58,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
// visitsCount and visitCount are deprecated. Only visits should work // visitsCount and visitCount are deprecated. Only visits should work
if (contains(['visits', 'visitsCount', 'visitCount'], $fieldName)) { if (contains(['visits', 'visitsCount', 'visitCount'], $fieldName)) {
// FIXME This query is inefficient. Debug it.
$qb->addSelect('COUNT(DISTINCT v) AS totalVisits') $qb->addSelect('COUNT(DISTINCT v) AS totalVisits')
->leftJoin('s.visits', 'v') ->leftJoin('s.visits', 'v')
->groupBy('s') ->groupBy('s')
@ -75,9 +75,11 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
'dateCreated' => 'dateCreated', 'dateCreated' => 'dateCreated',
'title' => 'title', 'title' => 'title',
]; ];
if (array_key_exists($fieldName, $fieldNameMap)) { $resolvedFieldName = $fieldNameMap[$fieldName] ?? null;
$qb->orderBy('s.' . $fieldNameMap[$fieldName], $order); if ($resolvedFieldName !== null) {
$qb->orderBy('s.' . $resolvedFieldName, $order);
} }
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
@ -194,10 +196,12 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
private function doShortCodeIsInUse(ShortUrlIdentifier $identifier, ?Specification $spec, ?int $lockMode): bool private function doShortCodeIsInUse(ShortUrlIdentifier $identifier, ?Specification $spec, ?int $lockMode): bool
{ {
$qb = $this->createFindOneQueryBuilder($identifier, $spec); $qb = $this->createFindOneQueryBuilder($identifier, $spec)->select('s.id');
$qb->select('s.id'); $query = $qb->getQuery();
$query = $qb->getQuery()->setLockMode($lockMode); if ($lockMode !== null) {
$query = $query->setLockMode($lockMode);
}
return $query->getOneOrNullResult() !== null; return $query->getOneOrNullResult() !== null;
} }

View file

@ -7,8 +7,7 @@ namespace Shlinkio\Shlink\Core\Util;
use Cocur\Slugify\SlugifyInterface; use Cocur\Slugify\SlugifyInterface;
use Symfony\Component\String\AbstractUnicodeString; use Symfony\Component\String\AbstractUnicodeString;
use Symfony\Component\String\Slugger\SluggerInterface; use Symfony\Component\String\Slugger\SluggerInterface;
use Symfony\Component\String\UnicodeString;
use function Symfony\Component\String\s;
class CocurSymfonySluggerBridge implements SluggerInterface class CocurSymfonySluggerBridge implements SluggerInterface
{ {
@ -18,6 +17,6 @@ class CocurSymfonySluggerBridge implements SluggerInterface
public function slug(string $string, string $separator = '-', ?string $locale = null): AbstractUnicodeString public function slug(string $string, string $separator = '-', ?string $locale = null): AbstractUnicodeString
{ {
return s($this->slugger->slugify($string, $separator)); return new UnicodeString($this->slugger->slugify($string, $separator));
} }
} }

View file

@ -27,6 +27,7 @@ class EditShortUrlTagsAction extends AbstractRestAction
public function handle(Request $request): Response public function handle(Request $request): Response
{ {
/** @var array $bodyParams */
$bodyParams = $request->getParsedBody(); $bodyParams = $request->getParsedBody();
if (! isset($bodyParams['tags'])) { if (! isset($bodyParams['tags'])) {

View file

@ -20,15 +20,9 @@ class CreateTagsAction extends AbstractRestAction
{ {
} }
/**
* Process an incoming server request and return a response, optionally delegating
* to the next middleware component to create the response.
*
*
* @throws \InvalidArgumentException
*/
public function handle(ServerRequestInterface $request): ResponseInterface public function handle(ServerRequestInterface $request): ResponseInterface
{ {
/** @var array $body */
$body = $request->getParsedBody(); $body = $request->getParsedBody();
$tags = $body['tags'] ?? []; $tags = $body['tags'] ?? [];

View file

@ -23,6 +23,7 @@ class UpdateTagAction extends AbstractRestAction
public function handle(ServerRequestInterface $request): ResponseInterface public function handle(ServerRequestInterface $request): ResponseInterface
{ {
/** @var array $body */
$body = $request->getParsedBody(); $body = $request->getParsedBody();
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request); $apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);

View file

@ -18,6 +18,7 @@ class DefaultShortCodesLengthMiddleware implements MiddlewareInterface
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
/** @var array $body */
$body = $request->getParsedBody(); $body = $request->getParsedBody();
if (! isset($body[ShortUrlInputFilter::SHORT_CODE_LENGTH])) { if (! isset($body[ShortUrlInputFilter::SHORT_CODE_LENGTH])) {
$body[ShortUrlInputFilter::SHORT_CODE_LENGTH] = $this->defaultShortCodesLength; $body[ShortUrlInputFilter::SHORT_CODE_LENGTH] = $this->defaultShortCodesLength;

View file

@ -17,8 +17,10 @@ class DropDefaultDomainFromRequestMiddleware implements MiddlewareInterface
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
/** @var array $body */
$body = $request->getParsedBody();
$request = $request->withQueryParams($this->sanitizeDomainFromPayload($request->getQueryParams())) $request = $request->withQueryParams($this->sanitizeDomainFromPayload($request->getQueryParams()))
->withParsedBody($this->sanitizeDomainFromPayload($request->getParsedBody())); ->withParsedBody($this->sanitizeDomainFromPayload($body));
return $handler->handle($request); return $handler->handle($request);
} }

View file

@ -32,6 +32,7 @@ class OverrideDomainMiddleware implements MiddlewareInterface
$domain = $this->domainService->getDomain($domainId); $domain = $this->domainService->getDomain($domainId);
if ($requestMethod === RequestMethodInterface::METHOD_POST) { if ($requestMethod === RequestMethodInterface::METHOD_POST) {
/** @var array $payload */
$payload = $request->getParsedBody(); $payload = $request->getParsedBody();
$payload[ShortUrlInputFilter::DOMAIN] = $domain->getAuthority(); $payload[ShortUrlInputFilter::DOMAIN] = $domain->getAuthority();

View file

@ -38,12 +38,12 @@ class ApiKeyService implements ApiKeyServiceInterface
private function buildApiKeyWithParams(?Chronos $expirationDate, ?string $name): ApiKey private function buildApiKeyWithParams(?Chronos $expirationDate, ?string $name): ApiKey
{ {
return match (true) { return match (true) {
$expirationDate === null && $name === null => ApiKey::create(),
$expirationDate !== null && $name !== null => ApiKey::fromMeta( $expirationDate !== null && $name !== null => ApiKey::fromMeta(
ApiKeyMeta::withNameAndExpirationDate($name, $expirationDate), ApiKeyMeta::withNameAndExpirationDate($name, $expirationDate),
), ),
$name === null => ApiKey::fromMeta(ApiKeyMeta::withExpirationDate($expirationDate)), $expirationDate !== null => ApiKey::fromMeta(ApiKeyMeta::withExpirationDate($expirationDate)),
default => ApiKey::fromMeta(ApiKeyMeta::withName($name)), $name !== null => ApiKey::fromMeta(ApiKeyMeta::withName($name)),
default => ApiKey::create(),
}; };
} }

View file

@ -1,5 +1,11 @@
includes:
- vendor/phpstan/phpstan-doctrine/extension.neon
- vendor/phpstan/phpstan-symfony/extension.neon
parameters: parameters:
checkMissingIterableValueType: false checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false checkGenericClassInNonGenericObjectType: false
ignoreErrors: symfony:
- '#If condition is always false#' console_application_loader: 'config/cli-app.php'
doctrine:
repositoryClass: Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository
objectManagerLoader: 'config/entity-manager.php'

View file

@ -2,5 +2,4 @@
declare(strict_types=1); declare(strict_types=1);
$run = require __DIR__ . '/../config/run.php'; (require __DIR__ . '/../config/run.php')();
$run();