Merge pull request #2009 from acelaya-forks/feature/doctrine-orm-3

Update to doctrine ORM 3.0
This commit is contained in:
Alejandro Celaya 2024-02-17 10:36:29 +01:00 committed by GitHub
commit c0a77b790d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 56 additions and 55 deletions

View File

@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
* [#1988](https://github.com/shlinkio/shlink/issues/1988) Remove dependency on `league\uri` package.
* [#1909](https://github.com/shlinkio/shlink/issues/1909) Update docker image to PHP 8.3.
* [#1786](https://github.com/shlinkio/shlink/issues/1786) Run API tests with RoadRunner by default.
* [#2008](https://github.com/shlinkio/shlink/issues/2008) Update to Doctrine ORM 3.0.
### Deprecated
* *Nothing*

View File

@ -20,12 +20,11 @@
"akrabat/ip-address-middleware": "^2.1",
"cakephp/chronos": "^3.0.2",
"doctrine/migrations": "^3.6",
"doctrine/orm": "^2.16",
"doctrine/orm": "^3.0",
"endroid/qr-code": "^4.8",
"friendsofphp/proxy-manager-lts": "^1.0",
"geoip2/geoip2": "^3.0",
"guzzlehttp/guzzle": "^7.5",
"happyr/doctrine-specification": "^2.0",
"jaybizzle/crawler-detect": "^1.2.116",
"laminas/laminas-config": "^3.8",
"laminas/laminas-config-aggregator": "^1.13",
@ -42,7 +41,8 @@
"pagerfanta/core": "^3.8",
"pugx/shortid-php": "^1.1",
"ramsey/uuid": "^4.7",
"shlinkio/shlink-common": "dev-main#2323ff3 as 6.0",
"shlinkio/doctrine-specification": "^2.1.1",
"shlinkio/shlink-common": "dev-main#178b332 as 6.0",
"shlinkio/shlink-config": "dev-main#6b287b3 as 2.6",
"shlinkio/shlink-event-dispatcher": "dev-main#46f5e21 as 4.0",
"shlinkio/shlink-importer": "^5.2.1",
@ -71,7 +71,7 @@
"phpunit/phpunit": "^10.4",
"roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~2.3.0",
"shlinkio/shlink-test-utils": "^3.10",
"shlinkio/shlink-test-utils": "^3.11",
"symfony/var-dumper": "^6.4",
"veewee/composer-run-parallel": "^1.3"
},

View File

@ -10,7 +10,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Exception;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;

View File

@ -6,7 +6,7 @@ namespace ShlinkMigrations;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\Migrations\AbstractMigration;
@ -32,7 +32,7 @@ class Version20160819142757 extends AbstractMigration
is_subclass_of($platformClass, MySQLPlatform::class) => $column
->setPlatformOption('charset', 'utf8mb4')
->setPlatformOption('collation', 'utf8mb4_bin'),
is_subclass_of($platformClass, SqlitePlatform::class) => $column->setPlatformOption('collate', 'BINARY'),
is_subclass_of($platformClass, SQLitePlatform::class) => $column->setPlatformOption('collate', 'BINARY'),
default => null,
};
}

View File

@ -8,7 +8,6 @@ use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use PDO;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Util\IpAddress;
@ -33,11 +32,11 @@ final class Version20180913205455 extends AbstractMigration
$st = $this->connection->executeQuery($qb->getSQL());
$qb = $this->connection->createQueryBuilder();
$qb->update('visits', 'v')
->set('v.remote_addr', ':obfuscatedAddr')
->where('v.id=:id');
$qb->update('visits')
->set('remote_addr', ':obfuscatedAddr')
->where('id=:id');
while ($row = $st->fetch(PDO::FETCH_ASSOC)) {
while ($row = $st->fetchAssociative()) {
$addr = $row['remote_addr'] ?? null;
if ($addr === null) {
continue;
@ -46,7 +45,7 @@ final class Version20180913205455 extends AbstractMigration
$qb->setParameters([
'id' => $row['id'],
'obfuscatedAddr' => $this->determineAddress((string) $addr),
])->execute();
])->executeQuery();
}
}

View File

@ -32,7 +32,7 @@ final class Version20200105165647 extends AbstractMigration
$qb = $this->connection->createQueryBuilder();
$qb->update('visit_locations')
->set($columnName, ':zeroValue')
->where($qb->expr()->orX(
->where($qb->expr()->or(
$qb->expr()->eq($columnName, ':emptyString'),
$qb->expr()->isNull($columnName),
))

View File

@ -29,10 +29,11 @@ final class Version20200323190014 extends AbstractMigration
->andWhere($qb->expr()->eq('region_name', ':emptyString'))
->andWhere($qb->expr()->eq('city_name', ':emptyString'))
->andWhere($qb->expr()->eq('timezone', ':emptyString'))
->andWhere($qb->expr()->eq('lat', 0))
->andWhere($qb->expr()->eq('lon', 0))
->andWhere($qb->expr()->eq('lat', ':latLong'))
->andWhere($qb->expr()->eq('lon', ':latLong'))
->setParameter('isEmpty', true)
->setParameter('emptyString', '')
->setParameter('latLong', 0)
->executeStatement();
}

View File

@ -5,8 +5,8 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Cake\Chronos\Chronos;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -33,7 +33,7 @@ final class Version20201102113208 extends AbstractMigration
public function postUp(Schema $schema): void
{
// If there's only one API key and it's active, link all existing URLs with it
// If there's only one API key, and it's active, link all existing URLs with it
$qb = $this->connection->createQueryBuilder();
$qb->select('id')
->from('api_keys')
@ -47,8 +47,7 @@ final class Version20201102113208 extends AbstractMigration
'expiration' => Chronos::now()->toDateTimeString(),
]);
/** @var Result $result */
$result = $qb->execute();
$result = $qb->executeQuery();
$id = $this->resolveOneApiKeyId($result);
if ($id === null) {
return;
@ -58,7 +57,7 @@ final class Version20201102113208 extends AbstractMigration
$qb->update('short_urls')
->set(self::API_KEY_COLUMN, ':apiKeyId')
->setParameter('apiKeyId', $id)
->execute();
->executeQuery();
}
private function resolveOneApiKeyId(Result $result): string|int|null

View File

@ -55,15 +55,15 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
if (OrderableField::isBasicField($fieldName)) {
$qb->orderBy('s.' . $fieldName, $order);
} elseif (OrderableField::isVisitsField($fieldName)) {
$leftJoinConditions = [$qb->expr()->eq('v.shortUrl', 's')];
if ($fieldName === OrderableField::NON_BOT_VISITS->value) {
$leftJoinConditions[] = $qb->expr()->eq('v.potentialBot', 'false');
}
// FIXME This query is inefficient.
// Diagnostic: It might need to use a sub-query, as done with the tags list query.
$qb->addSelect('COUNT(DISTINCT v)')
->leftJoin('s.visits', 'v', Join::WITH, $qb->expr()->andX(
$qb->expr()->eq('v.shortUrl', 's'),
$fieldName === OrderableField::NON_BOT_VISITS->value
? $qb->expr()->eq('v.potentialBot', 'false')
: null,
))
->leftJoin('s.visits', 'v', Join::WITH, $qb->expr()->andX(...$leftJoinConditions))
->groupBy('s')
->orderBy('COUNT(DISTINCT v)', $order);
}

View File

@ -72,7 +72,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
/**
* @param LockMode::PESSIMISTIC_WRITE|null $lockMode
*/
private function doShortCodeIsInUse(ShortUrlIdentifier $identifier, ?Specification $spec, ?int $lockMode): bool
private function doShortCodeIsInUse(ShortUrlIdentifier $identifier, ?Specification $spec, ?LockMode $lockMode): bool
{
$qb = $this->createFindOneQueryBuilder($identifier, $spec)->select('s.id');
$query = $qb->getQuery();

View File

@ -79,6 +79,7 @@ class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInt
return new Collections\ArrayCollection(array_map(function (string $tagName) use ($repo): Tag {
$this->lock($this->tagLocks, 'tag_' . $tagName);
/** @var Tag|null $existingTag */
$existingTag = $repo->findOneBy(['name' => $tagName]);
if ($existingTag) {
$this->releaseLock($this->tagLocks, 'tag_' . $tagName);

View File

@ -82,12 +82,12 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
: $visitsSubQb->expr()->and(
$commonJoinCondition,
$visitsSubQb->expr()->eq('v.potential_bot', $conn->quote('0')),
);
)->__toString();
return $visitsSubQb
->select('st.tag_id AS tag_id', 'COUNT(DISTINCT v.id) AS ' . $aggregateAlias)
->from('visits', 'v')
->join('v', 'short_urls', 's', $visitsJoin) // @phpstan-ignore-line
->join('v', 'short_urls', 's', $visitsJoin)
->join('s', 'short_urls_in_tags', 'st', $visitsSubQb->expr()->eq('st.short_url_id', 's.id'))
->groupBy('st.tag_id');
};

View File

@ -14,7 +14,7 @@ use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
use Shlinkio\Shlink\Core\Domain\DomainService;
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepositoryInterface;
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository;
use Shlinkio\Shlink\Core\Exception\DomainNotFoundException;
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
@ -34,7 +34,7 @@ class DomainServiceTest extends TestCase
#[Test, DataProvider('provideExcludedDomains')]
public function listDomainsDelegatesIntoRepository(array $domains, array $expectedResult, ?ApiKey $apiKey): void
{
$repo = $this->createMock(DomainRepositoryInterface::class);
$repo = $this->createMock(DomainRepository::class);
$repo->expects($this->once())->method('findDomains')->with($apiKey)->willReturn($domains);
$this->em->expects($this->once())->method('getRepository')->with(Domain::class)->willReturn($repo);
@ -126,7 +126,7 @@ class DomainServiceTest extends TestCase
public function getOrCreateAlwaysPersistsDomain(?Domain $foundDomain, ?ApiKey $apiKey): void
{
$authority = 'example.com';
$repo = $this->createMock(DomainRepositoryInterface::class);
$repo = $this->createMock(DomainRepository::class);
$repo->method('findOneByAuthority')->with($authority, $apiKey)->willReturn(
$foundDomain,
);
@ -148,7 +148,7 @@ class DomainServiceTest extends TestCase
$domain = Domain::withAuthority($authority);
$domain->setId('1');
$apiKey = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forDomain($domain)));
$repo = $this->createMock(DomainRepositoryInterface::class);
$repo = $this->createMock(DomainRepository::class);
$repo->method('findOneByAuthority')->with($authority, $apiKey)->willReturn(null);
$this->em->expects($this->once())->method('getRepository')->with(Domain::class)->willReturn($repo);
$this->em->expects($this->never())->method('persist');
@ -163,7 +163,7 @@ class DomainServiceTest extends TestCase
public function configureNotFoundRedirectsConfiguresFetchedDomain(?Domain $foundDomain, ?ApiKey $apiKey): void
{
$authority = 'example.com';
$repo = $this->createMock(DomainRepositoryInterface::class);
$repo = $this->createMock(DomainRepository::class);
$repo->method('findOneByAuthority')->with($authority, $apiKey)->willReturn($foundDomain);
$this->em->expects($this->once())->method('getRepository')->with(Domain::class)->willReturn($repo);
$this->em->expects($this->once())->method('persist')->with($foundDomain ?? $this->isInstanceOf(Domain::class));

View File

@ -16,12 +16,12 @@ use RuntimeException;
use Shlinkio\Shlink\Core\Importer\ImportedLinksProcessor;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortCodeUniquenessHelperInterface;
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepositoryInterface;
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepository;
use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver;
use Shlinkio\Shlink\Core\Util\DoctrineBatchHelperInterface;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Repository\VisitRepository;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkOrphanVisit;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkVisit;
@ -42,13 +42,13 @@ class ImportedLinksProcessorTest extends TestCase
private ImportedLinksProcessor $processor;
private MockObject & EntityManagerInterface $em;
private MockObject & ShortCodeUniquenessHelperInterface $shortCodeHelper;
private MockObject & ShortUrlRepositoryInterface $repo;
private MockObject & ShortUrlRepository $repo;
private MockObject & StyleInterface $io;
protected function setUp(): void
{
$this->em = $this->createMock(EntityManagerInterface::class);
$this->repo = $this->createMock(ShortUrlRepositoryInterface::class);
$this->repo = $this->createMock(ShortUrlRepository::class);
$this->shortCodeHelper = $this->createMock(ShortCodeUniquenessHelperInterface::class);
$batchHelper = $this->createMock(DoctrineBatchHelperInterface::class);
@ -281,7 +281,7 @@ class ImportedLinksProcessorTest extends TestCase
sprintf('<info>Imported %s</info> orphan visits.', $expectedImportedVisits),
);
$visitRepo = $this->createMock(VisitRepositoryInterface::class);
$visitRepo = $this->createMock(VisitRepository::class);
$visitRepo->expects($importOrphanVisits ? $this->once() : $this->never())->method(
'findMostRecentOrphanVisit',
)->willReturn($lastOrphanVisit);

View File

@ -11,11 +11,11 @@ use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepositoryInterface;
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
use Shlinkio\Shlink\Core\Tag\Entity\Tag;
use Shlinkio\Shlink\Core\Tag\Repository\TagRepositoryInterface;
use Shlinkio\Shlink\Core\Tag\Repository\TagRepository;
use function count;
@ -50,7 +50,7 @@ class PersistenceShortUrlRelationResolverTest extends TestCase
#[Test, DataProvider('provideFoundDomains')]
public function findsOrCreatesDomainWhenValueIsProvided(?Domain $foundDomain, string $authority): void
{
$repo = $this->createMock(DomainRepositoryInterface::class);
$repo = $this->createMock(DomainRepository::class);
$repo->expects($this->once())->method('findOneBy')->with(['authority' => $authority])->willReturn($foundDomain);
$this->em->expects($this->once())->method('getRepository')->with(Domain::class)->willReturn($repo);
@ -78,7 +78,7 @@ class PersistenceShortUrlRelationResolverTest extends TestCase
// One of the tags will already exist. The rest will be new
$expectedPersistedTags = $expectedLookedOutTags - 1;
$tagRepo = $this->createMock(TagRepositoryInterface::class);
$tagRepo = $this->createMock(TagRepository::class);
$tagRepo->expects($this->exactly($expectedLookedOutTags))->method('findOneBy')->with(
$this->isType('array'),
)->willReturnCallback(function (array $criteria): ?Tag {
@ -116,7 +116,7 @@ class PersistenceShortUrlRelationResolverTest extends TestCase
#[Test]
public function newDomainsAreMemoizedUntilStateIsCleared(): void
{
$repo = $this->createMock(DomainRepositoryInterface::class);
$repo = $this->createMock(DomainRepository::class);
$repo->expects($this->exactly(3))->method('findOneBy')->with($this->isType('array'))->willReturn(null);
$this->em->method('getRepository')->with(Domain::class)->willReturn($repo);
@ -135,7 +135,7 @@ class PersistenceShortUrlRelationResolverTest extends TestCase
#[Test]
public function newTagsAreMemoizedUntilStateIsCleared(): void
{
$tagRepo = $this->createMock(TagRepositoryInterface::class);
$tagRepo = $this->createMock(TagRepository::class);
$tagRepo->expects($this->exactly(6))->method('findOneBy')->with($this->isType('array'))->willReturn(null);
$this->em->method('getRepository')->with(Tag::class)->willReturn($tagRepo);

View File

@ -18,7 +18,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepositoryInterface;
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepository;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolver;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
@ -32,12 +32,12 @@ class ShortUrlResolverTest extends TestCase
{
private ShortUrlResolver $urlResolver;
private MockObject & EntityManagerInterface $em;
private MockObject & ShortUrlRepositoryInterface $repo;
private MockObject & ShortUrlRepository $repo;
protected function setUp(): void
{
$this->em = $this->createMock(EntityManagerInterface::class);
$this->repo = $this->createMock(ShortUrlRepositoryInterface::class);
$this->repo = $this->createMock(ShortUrlRepository::class);
$this->urlResolver = new ShortUrlResolver($this->em, new UrlShortenerOptions());
}

View File

@ -19,7 +19,7 @@ use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepositoryInterface;
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepository;
use Shlinkio\Shlink\Core\Tag\Entity\Tag;
use Shlinkio\Shlink\Core\Tag\Repository\TagRepository;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
@ -90,7 +90,7 @@ class VisitsStatsHelperTest extends TestCase
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode);
$spec = $apiKey?->spec();
$repo = $this->createMock(ShortUrlRepositoryInterface::class);
$repo = $this->createMock(ShortUrlRepository::class);
$repo->expects($this->once())->method('shortCodeIsInUse')->with($identifier, $spec)->willReturn(true);
$list = array_map(
@ -123,7 +123,7 @@ class VisitsStatsHelperTest extends TestCase
$shortCode = '123ABC';
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode);
$repo = $this->createMock(ShortUrlRepositoryInterface::class);
$repo = $this->createMock(ShortUrlRepository::class);
$repo->expects($this->once())->method('shortCodeIsInUse')->with($identifier, null)->willReturn(false);
$this->em->expects($this->once())->method('getRepository')->with(ShortUrl::class)->willReturn($repo);

View File

@ -14,7 +14,7 @@ use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
use Shlinkio\Shlink\Rest\ApiKey\Repository\ApiKeyRepositoryInterface;
use Shlinkio\Shlink\Rest\ApiKey\Repository\ApiKeyRepository;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
@ -22,12 +22,12 @@ class ApiKeyServiceTest extends TestCase
{
private ApiKeyService $service;
private MockObject & EntityManager $em;
private MockObject & ApiKeyRepositoryInterface $repo;
private MockObject & ApiKeyRepository $repo;
protected function setUp(): void
{
$this->em = $this->createMock(EntityManager::class);
$this->repo = $this->createMock(ApiKeyRepositoryInterface::class);
$this->repo = $this->createMock(ApiKeyRepository::class);
$this->service = new ApiKeyService($this->em);
}