Skip to content

Commit 59eb865

Browse files
committed
Delete distribution files when removing related entities from the database
1 parent d2deddf commit 59eb865

10 files changed

Lines changed: 375 additions & 2 deletions

File tree

ARCHITECTURE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ src/
3434
├── DependencyInjection/ # Symfony dependency injection configuration
3535
├── Doctrine/
3636
│ ├── Entity/ # Doctrine ORM entities
37+
│ ├── EventListener/ # Doctrine entity lifecycle listeners
3738
│ ├── Repository/ # Doctrine repositories
3839
│ ├── Type/ # Custom Doctrine types
3940
│ └── DataFixtures/ # Database fixtures
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CodedMonkey\Dirigent\Doctrine\EventListener;
6+
7+
use CodedMonkey\Dirigent\Doctrine\Entity\Distribution;
8+
use CodedMonkey\Dirigent\Package\PackageDistributionResolver;
9+
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
10+
use Doctrine\ORM\Events;
11+
12+
#[AsEntityListener(Events::preRemove, entity: Distribution::class)]
13+
readonly class DistributionListener
14+
{
15+
public function __construct(
16+
private PackageDistributionResolver $distributionResolver,
17+
) {
18+
}
19+
20+
public function preRemove(Distribution $distribution): void
21+
{
22+
$this->distributionResolver->remove($distribution);
23+
}
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CodedMonkey\Dirigent\Doctrine\EventListener;
6+
7+
use CodedMonkey\Dirigent\Doctrine\Entity\Metadata;
8+
use CodedMonkey\Dirigent\Package\PackageDistributionResolver;
9+
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
10+
use Doctrine\ORM\Events;
11+
12+
#[AsEntityListener(Events::preRemove, entity: Metadata::class)]
13+
readonly class MetadataListener
14+
{
15+
public function __construct(
16+
private PackageDistributionResolver $distributionResolver,
17+
) {
18+
}
19+
20+
public function preRemove(Metadata $metadata): void
21+
{
22+
$this->distributionResolver->removeMetadata($metadata);
23+
}
24+
}

src/Doctrine/EventListener/PackageListener.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use CodedMonkey\Dirigent\Doctrine\Entity\Package;
88
use CodedMonkey\Dirigent\Doctrine\Repository\PackageRepository;
99
use CodedMonkey\Dirigent\Message\RemovePackageProvider;
10+
use CodedMonkey\Dirigent\Package\PackageDistributionResolver;
1011
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
1112
use Doctrine\ORM\Event\PreRemoveEventArgs;
1213
use Doctrine\ORM\Events;
@@ -17,6 +18,7 @@
1718
{
1819
public function __construct(
1920
private MessageBusInterface $messenger,
21+
private PackageDistributionResolver $distributionResolver,
2022
) {
2123
}
2224

@@ -28,6 +30,9 @@ public function preRemove(Package $package, PreRemoveEventArgs $event): void
2830
// Delete existing package links
2931
$repository->deletePackageLinks($package);
3032

33+
// Delete mirrored package distributions
34+
$this->distributionResolver->removePackage($package);
35+
3136
// Remove package provider
3237
$this->messenger->dispatch(new RemovePackageProvider($package->getId()));
3338
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CodedMonkey\Dirigent\Doctrine\EventListener;
6+
7+
use CodedMonkey\Dirigent\Doctrine\Entity\Version;
8+
use CodedMonkey\Dirigent\Package\PackageDistributionResolver;
9+
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
10+
use Doctrine\ORM\Events;
11+
12+
#[AsEntityListener(Events::preRemove, entity: Version::class)]
13+
readonly class VersionListener
14+
{
15+
public function __construct(
16+
private PackageDistributionResolver $distributionResolver,
17+
) {
18+
}
19+
20+
public function preRemove(Version $version): void
21+
{
22+
$this->distributionResolver->removeVersion($version);
23+
}
24+
}

src/Doctrine/Repository/DistributionRepository.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use CodedMonkey\Dirigent\Doctrine\Entity\Distribution;
88
use CodedMonkey\Dirigent\Doctrine\Entity\Metadata;
9+
use CodedMonkey\Dirigent\Doctrine\Entity\Package;
10+
use CodedMonkey\Dirigent\Doctrine\Entity\Version;
911
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
1012
use Doctrine\Persistence\ManagerRegistry;
1113

@@ -49,4 +51,42 @@ public function findOneByMetadataAndType(Metadata $metadata, string $type): ?Dis
4951
'type' => $type,
5052
]);
5153
}
54+
55+
/**
56+
* @return Distribution[]
57+
*/
58+
public function findByMetadata(Metadata $metadata): array
59+
{
60+
return $this->createQueryBuilder('distribution')
61+
->where('distribution.metadata = :metadata')
62+
->setParameter('metadata', $metadata)
63+
->getQuery()
64+
->getResult();
65+
}
66+
67+
/**
68+
* @return Distribution[]
69+
*/
70+
public function findByPackage(Package $package): array
71+
{
72+
return $this->createQueryBuilder('distribution')
73+
->join('distribution.metadata', 'metadata')
74+
->where('metadata.package = :package')
75+
->setParameter('package', $package)
76+
->getQuery()
77+
->getResult();
78+
}
79+
80+
/**
81+
* @return Distribution[]
82+
*/
83+
public function findByVersion(Version $version): array
84+
{
85+
return $this->createQueryBuilder('distribution')
86+
->join('distribution.metadata', 'metadata')
87+
->where('metadata.version = :version')
88+
->setParameter('version', $version)
89+
->getQuery()
90+
->getResult();
91+
}
5292
}

src/Package/PackageDistributionResolver.php

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
use CodedMonkey\Dirigent\Composer\ComposerClient;
88
use CodedMonkey\Dirigent\Doctrine\Entity\Distribution;
99
use CodedMonkey\Dirigent\Doctrine\Entity\Metadata;
10+
use CodedMonkey\Dirigent\Doctrine\Entity\Package;
11+
use CodedMonkey\Dirigent\Doctrine\Entity\Version;
1012
use CodedMonkey\Dirigent\Doctrine\Repository\DistributionRepository;
1113
use CodedMonkey\Dirigent\Message\ResolveDistribution;
1214
use Symfony\Component\DependencyInjection\Attribute\Autowire;
1315
use Symfony\Component\Filesystem\Filesystem;
1416
use Symfony\Component\Lock\LockFactory;
17+
use Symfony\Component\Lock\SharedLockInterface;
1518
use Symfony\Component\Messenger\MessageBusInterface;
1619
use Symfony\Component\Messenger\Stamp\TransportNamesStamp;
1720

@@ -51,6 +54,51 @@ public function path(Metadata $metadata, string $type): string
5154
return "{$this->storagePath}/{$packageName}/{$versionName}-r{$revision}-{$reference}.{$type}";
5255
}
5356

57+
public function remove(Distribution $distribution): void
58+
{
59+
$path = $this->path($distribution->getMetadata(), $distribution->getType());
60+
$lock = $this->createDistributionLock($path);
61+
62+
try {
63+
$this->filesystem->remove($path);
64+
65+
// Remove the package directory if it's empty
66+
$packageDirectory = dirname($path);
67+
if (is_dir($packageDirectory) && !new \FilesystemIterator($packageDirectory)->valid()) {
68+
$this->filesystem->remove($packageDirectory);
69+
}
70+
71+
// Remove the vendor directory if it's empty
72+
$vendorDirectory = dirname($packageDirectory);
73+
if (is_dir($vendorDirectory) && !new \FilesystemIterator($vendorDirectory)->valid()) {
74+
$this->filesystem->remove($vendorDirectory);
75+
}
76+
} finally {
77+
$lock->release();
78+
}
79+
}
80+
81+
public function removeMetadata(Metadata $metadata): void
82+
{
83+
foreach ($this->distributionRepository->findByMetadata($metadata) as $distribution) {
84+
$this->remove($distribution);
85+
}
86+
}
87+
88+
public function removePackage(Package $package): void
89+
{
90+
foreach ($this->distributionRepository->findByPackage($package) as $distribution) {
91+
$this->remove($distribution);
92+
}
93+
}
94+
95+
public function removeVersion(Version $version): void
96+
{
97+
foreach ($this->distributionRepository->findByVersion($version) as $distribution) {
98+
$this->remove($distribution);
99+
}
100+
}
101+
54102
public function resolve(Metadata $metadata, string $type, bool $async): bool
55103
{
56104
if (!$this->mirrorDistributions) {
@@ -84,8 +132,7 @@ public function resolve(Metadata $metadata, string $type, bool $async): bool
84132
$distributionUrl = $metadata->getDistributionUrl();
85133
$path = $this->path($metadata, $type);
86134

87-
$lock = $this->lockFactory->createLock('distribution.' . hash('sha256', $path), ttl: null);
88-
$lock->acquire(blocking: true);
135+
$lock = $this->createDistributionLock($path);
89136

90137
try {
91138
if ($this->fileExists($path)) {
@@ -112,6 +159,14 @@ public function resolve(Metadata $metadata, string $type, bool $async): bool
112159
}
113160
}
114161

162+
private function createDistributionLock(string $path): SharedLockInterface
163+
{
164+
$lock = $this->lockFactory->createLock('distribution.' . hash('sha256', $path), ttl: null);
165+
$lock->acquire(blocking: true);
166+
167+
return $lock;
168+
}
169+
115170
private function fileExists(string $path): bool
116171
{
117172
return $this->filesystem->exists($path);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CodedMonkey\Dirigent\Tests\FunctionalTests\Doctrine\Entity;
6+
7+
use CodedMonkey\Dirigent\Doctrine\Entity\Distribution;
8+
use CodedMonkey\Dirigent\Doctrine\Entity\Metadata;
9+
use CodedMonkey\Dirigent\Package\PackageDistributionResolver;
10+
use CodedMonkey\Dirigent\Tests\Helper\EntityManagerTestTrait;
11+
use CodedMonkey\Dirigent\Tests\Helper\KernelTestCaseTrait;
12+
use CodedMonkey\Dirigent\Tests\Helper\MockEntityFactoryTrait;
13+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
14+
use Symfony\Component\Filesystem\Filesystem;
15+
16+
class DistributionDeletionTest extends KernelTestCase
17+
{
18+
use EntityManagerTestTrait;
19+
use KernelTestCaseTrait;
20+
use MockEntityFactoryTrait;
21+
22+
public function testDeletingDistributionDeletesDistributionFile(): void
23+
{
24+
[$package, $version, $metadata] = $this->createMockPackageWithMetadata();
25+
[$distribution, $path] = $this->createDistributionFile($metadata);
26+
$this->persistEntities($package, $version, $metadata, $distribution);
27+
28+
$this->removeEntities($distribution);
29+
30+
self::assertFileDoesNotExist($path);
31+
}
32+
33+
public function testDeletingMetadataDeletesDistributionFile(): void
34+
{
35+
[$package, $version, $metadata] = $this->createMockPackageWithMetadata();
36+
[$distribution, $path] = $this->createDistributionFile($metadata);
37+
38+
// Metadata can't be deleted if it's the current metadata of a version, so we replace it
39+
$currentMetadata = $this->createMockMetadata($version);
40+
$version->setCurrentMetadata($currentMetadata);
41+
42+
$this->persistEntities($package, $version, $metadata, $currentMetadata, $distribution);
43+
44+
$this->removeEntities($metadata);
45+
46+
self::assertFileDoesNotExist($path);
47+
}
48+
49+
public function testDeletingPackageDeletesDistributionFile(): void
50+
{
51+
[$package, $version, $metadata] = $this->createMockPackageWithMetadata();
52+
[$distribution, $path] = $this->createDistributionFile($metadata);
53+
$this->persistEntities($package, $version, $metadata, $distribution);
54+
55+
$this->removeEntities($package);
56+
57+
self::assertFileDoesNotExist($path);
58+
}
59+
60+
public function testDeletingVersionDeletesDistributionFile(): void
61+
{
62+
[$package, $version, $metadata] = $this->createMockPackageWithMetadata();
63+
[$distribution, $path] = $this->createDistributionFile($metadata);
64+
$this->persistEntities($package, $version, $metadata, $distribution);
65+
66+
$this->removeEntities($version);
67+
68+
self::assertFileDoesNotExist($path);
69+
}
70+
71+
/**
72+
* @return array{Distribution, string}
73+
*/
74+
private function createDistributionFile(Metadata $metadata): array
75+
{
76+
$metadata->setDistributionReference('reference');
77+
78+
$distribution = new Distribution($metadata, 'zip');
79+
$distribution->setResolvedAt();
80+
81+
$path = $this->getService(PackageDistributionResolver::class)->path($metadata, $distribution->getType());
82+
new Filesystem()->dumpFile($path, 'distribution');
83+
84+
return [$distribution, $path];
85+
}
86+
}

tests/Helper/EntityManagerTestTrait.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ protected function persistEntities(...$entities): void
4242
$entityManager->flush();
4343
}
4444

45+
protected function removeEntities(...$entities): void
46+
{
47+
$entityManager = $this->getService(EntityManagerInterface::class);
48+
49+
foreach ($entities as $entity) {
50+
$entityManager->remove($entity);
51+
}
52+
53+
$entityManager->flush();
54+
}
55+
4556
protected function clearEntities(): void
4657
{
4758
$this->getService(EntityManagerInterface::class)->clear();

0 commit comments

Comments
 (0)