Skip to content

Commit 72c16d4

Browse files
committed
Create package link pages
1 parent a680986 commit 72c16d4

File tree

7 files changed

+226
-9
lines changed

7 files changed

+226
-9
lines changed

src/Controller/Dashboard/DashboardPackagesInfoController.php

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,23 @@
44

55
use CodedMonkey\Dirigent\Attribute\IsGrantedAccess;
66
use CodedMonkey\Dirigent\Doctrine\Entity\Package;
7+
use CodedMonkey\Dirigent\Doctrine\Entity\PackageProvideLink;
8+
use CodedMonkey\Dirigent\Doctrine\Entity\PackageRequireLink;
9+
use CodedMonkey\Dirigent\Doctrine\Entity\PackageSuggestLink;
710
use CodedMonkey\Dirigent\Doctrine\Repository\PackageRepository;
11+
use CodedMonkey\Dirigent\EasyAdmin\PackagePaginator;
812
use Composer\Semver\VersionParser;
13+
use Doctrine\ORM\EntityManagerInterface;
14+
use Doctrine\ORM\QueryBuilder;
915
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
16+
use Symfony\Component\HttpFoundation\Request;
1017
use Symfony\Component\HttpFoundation\Response;
1118
use Symfony\Component\Routing\Attribute\Route;
1219

1320
class DashboardPackagesInfoController extends AbstractController
1421
{
1522
public function __construct(
23+
private readonly EntityManagerInterface $entityManager,
1624
private readonly PackageRepository $packageRepository,
1725
) {
1826
}
@@ -24,10 +32,7 @@ public function info(string $packageName): Response
2432
$package = $this->packageRepository->findOneBy(['name' => $packageName]);
2533
$version = $package->getLatestVersion();
2634

27-
return $this->render('dashboard/packages/package_info.html.twig', [
28-
'package' => $package,
29-
'version' => $version,
30-
]);
35+
return $this->versionInfo($packageName, $version->getNormalizedVersion());
3136
}
3237

3338
#[Route('/packages/{packageName}/v/{packageVersion}', name: 'dashboard_packages_version_info', requirements: ['packageName' => '[a-z0-9_.-]+/[a-z0-9_.-]+'])]
@@ -37,9 +42,19 @@ public function versionInfo(string $packageName, string $packageVersion): Respon
3742
$package = $this->packageRepository->findOneBy(['name' => $packageName]);
3843
$version = $package->getVersion((new VersionParser())->normalize($packageVersion));
3944

45+
$dependentCount = $this->entityManager->getRepository(PackageRequireLink::class)->count(['linkedPackageName' => $package->getName()]);
46+
$implementationCount = $this->entityManager->getRepository(PackageProvideLink::class)->count(['linkedPackageName' => $package->getName(), 'implementation' => true]);
47+
$providerCount = $this->entityManager->getRepository(PackageProvideLink::class)->count(['linkedPackageName' => $package->getName(), 'implementation' => false]);
48+
$suggesterCount = $this->entityManager->getRepository(PackageSuggestLink::class)->count(['linkedPackageName' => $package->getName()]);
49+
4050
return $this->render('dashboard/packages/package_info.html.twig', [
4151
'package' => $package,
4252
'version' => $version,
53+
54+
'dependentCount' => $dependentCount,
55+
'implementationCount' => $implementationCount,
56+
'providerCount' => $providerCount,
57+
'suggesterCount' => $suggesterCount,
4358
]);
4459
}
4560

@@ -58,6 +73,83 @@ public function versions(string $packageName): Response
5873
]);
5974
}
6075

76+
#[Route('/packages/{packageName}/dependents', name: 'dashboard_packages_dependents', requirements: ['packageName' => '[a-z0-9_.-]+/[a-z0-9_.-]+'])]
77+
#[IsGrantedAccess]
78+
public function dependents(Request $request, string $packageName): Response
79+
{
80+
$package = $this->packageRepository->findOneBy(['name' => $packageName]);
81+
82+
return $this->packageLinks($request, $package, PackageProvideLink::class, 'Dependents');
83+
}
84+
85+
#[Route('/packages/{packageName}/implementations', name: 'dashboard_packages_implementations', requirements: ['packageName' => '[a-z0-9_.-]+/[a-z0-9_.-]+'])]
86+
#[IsGrantedAccess]
87+
public function implementations(Request $request, string $packageName): Response
88+
{
89+
$package = $this->packageRepository->findOneBy(['name' => $packageName]);
90+
91+
$providerRepository = $this->entityManager->getRepository(PackageProvideLink::class);
92+
$queryBuilder = $providerRepository->createQueryBuilder('provider');
93+
$queryBuilder
94+
->leftJoin('provider.package', 'package')
95+
->andWhere('provider.linkedPackageName = :packageName')
96+
->andWhere('provider.implementation = true')
97+
->setParameter('packageName', $package->getName())
98+
->addOrderBy('package.name', 'ASC');
99+
100+
return $this->packageLinks($request, $package, PackageProvideLink::class, 'Implementations', queryBuilder: $queryBuilder);
101+
}
102+
103+
#[Route('/packages/{packageName}/providers', name: 'dashboard_packages_providers', requirements: ['packageName' => '[a-z0-9_.-]+/[a-z0-9_.-]+'])]
104+
#[IsGrantedAccess]
105+
public function providers(Request $request, string $packageName): Response
106+
{
107+
$package = $this->packageRepository->findOneBy(['name' => $packageName]);
108+
109+
$providerRepository = $this->entityManager->getRepository(PackageProvideLink::class);
110+
$queryBuilder = $providerRepository->createQueryBuilder('provider');
111+
$queryBuilder
112+
->leftJoin('provider.package', 'package')
113+
->andWhere('provider.linkedPackageName = :packageName')
114+
->andWhere('provider.implementation = false')
115+
->setParameter('packageName', $package->getName())
116+
->addOrderBy('package.name', 'ASC');
117+
118+
return $this->packageLinks($request, $package, PackageProvideLink::class, 'Providers', queryBuilder: $queryBuilder);
119+
}
120+
121+
#[Route('/packages/{packageName}/suggesters', name: 'dashboard_packages_suggesters', requirements: ['packageName' => '[a-z0-9_.-]+/[a-z0-9_.-]+'])]
122+
#[IsGrantedAccess]
123+
public function suggesters(Request $request, string $packageName): Response
124+
{
125+
$package = $this->packageRepository->findOneBy(['name' => $packageName]);
126+
127+
return $this->packageLinks($request, $package, PackageSuggestLink::class, 'Suggesters');
128+
}
129+
130+
private function packageLinks(Request $request, Package $package, string $linkClass, string $title, ?QueryBuilder $queryBuilder = null): Response
131+
{
132+
if (!$queryBuilder) {
133+
$dependentRepository = $this->entityManager->getRepository($linkClass);
134+
$queryBuilder = $dependentRepository->createQueryBuilder('link');
135+
$queryBuilder
136+
->leftJoin('link.package', 'package')
137+
->andWhere('link.linkedPackageName = :packageName')
138+
->setParameter('packageName', $package->getName())
139+
->addOrderBy('package.name', 'ASC');
140+
}
141+
142+
$paginator = PackagePaginator::fromRequest($request, $queryBuilder, $this->container->get('router'));
143+
$packageLinks = $paginator->getResults();
144+
145+
return $this->render('dashboard/packages/package_links.html.twig', [
146+
'package' => $package,
147+
'packageLinks' => $packageLinks,
148+
'paginator' => $paginator,
149+
'title' => $title,
150+
]);
151+
}
152+
61153
#[Route('/packages/{packageName}/statistics', name: 'dashboard_packages_statistics', requirements: ['packageName' => '[a-z0-9_.-]+/[a-z0-9_.-]+'])]
62154
#[IsGrantedAccess]
63155
public function statistics(string $packageName): Response

src/Doctrine/Entity/VersionProvideLink.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,14 @@ class VersionProvideLink extends AbstractVersionLink
1010
#[ORM\ManyToOne(targetEntity: Version::class, inversedBy: 'provide')]
1111
#[ORM\JoinColumn(nullable: false)]
1212
protected Version $version;
13+
14+
public function isImplementation(): bool
15+
{
16+
return str_ends_with($this->getLinkedPackageName(), '-implementation');
17+
}
18+
19+
public function getImplementedPackageName(): string
20+
{
21+
return substr($this->getLinkedPackageName(), 0, -15);
22+
}
1323
}

templates/dashboard/packages/_package_header.html.twig

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
<a {% if currentPage == 'versions' %}class="nav-link active" aria-current="page"{% else %}class="nav-link text-primary"{% endif%} href="{{ packageVersionsUrl }}">{{ 'Versions'|trans }}</a>
1111
</li>
1212
<li class="nav-item">
13-
<span class="nav-link disabled">{{ 'Dependants'|trans }}</span>
13+
{% set packageDependentsUrl = path('dashboard_packages_dependents', {packageName: package.name}) %}
14+
<a {% if currentPage == 'dependents' %}class="nav-link active" aria-current="page"{% else %}class="nav-link text-primary"{% endif%} href="{{ packageDependentsUrl }}">{{ 'Dependents'|trans }}</a>
1415
</li>
1516
<li class="nav-item">
16-
{% set packageVersionsUrl = path('dashboard_packages_statistics', {packageName: package.name}) %}
17-
<a {% if currentPage == 'statistics' %}class="nav-link active" aria-current="page"{% else %}class="nav-link text-primary"{% endif%} href="{{ packageVersionsUrl }}">{{ 'Statistics'|trans }}</a>
17+
{% set packageStatisticsUrl = path('dashboard_packages_statistics', {packageName: package.name}) %}
18+
<a {% if currentPage == 'statistics' %}class="nav-link active" aria-current="page"{% else %}class="nav-link text-primary"{% endif%} href="{{ packageStatisticsUrl }}">{{ 'Statistics'|trans }}</a>
1819
</li>
1920
</ul>

templates/dashboard/packages/package_info.html.twig

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@
4545
<div><a href="{{ version.source.url }}">Source</a></div>
4646
{% endif %}
4747
<div>Installs: {{ package.installations.total }}</div>
48+
{% set packageDependentsUrl = path('dashboard_packages_dependents', {packageName: package.name}) %}
49+
<div><a href="{{ packageDependentsUrl }}">{{ 'Dependents'|trans }}</a>: {{ dependentCount }}</div>
50+
{% set packageSuggestersUrl = path('dashboard_packages_suggesters', {packageName: package.name}) %}
51+
<div><a href="{{ packageSuggestersUrl }}">{{ 'Suggesters'|trans }}</a>: {{ suggesterCount }}</div>
52+
{% set packageImplementationsUrl = path('dashboard_packages_implementations', {packageName: package.name}) %}
53+
<div><a href="{{ packageImplementationsUrl }}">{{ 'Implementations'|trans }}</a>: {{ implementationCount }}</div>
54+
{% set packageProvidersUrl = path('dashboard_packages_providers', {packageName: package.name}) %}
55+
<div><a href="{{ packageProvidersUrl }}">{{ 'Providers'|trans }}</a>: {{ providerCount }}</div>
4856
{% if version.license %}
4957
<div>License: {{ version.license|join(', ') }}</div>
5058
{% else %}
@@ -64,7 +72,7 @@
6472
<div class="row border-bottom pb-3 mb-3">
6573
{{ _self.linkBlock('requires', version.require) }}
6674
{{ _self.linkBlock('requires (dev)', version.devRequire) }}
67-
{{ _self.linkBlock('provides', version.provide) }}
75+
{{ _self.provideBlock(version.provide) }}
6876
{{ _self.linkBlock('suggests', version.suggest) }}
6977
{{ _self.linkBlock('conflicts', version.conflict) }}
7078
{{ _self.linkBlock('replaces', version.replace) }}
@@ -94,3 +102,23 @@
94102
</ul>
95103
</div>
96104
{% endmacro %}
105+
106+
{% macro provideBlock(links) %}
107+
<div class="col-md-6 col-lg-4">
108+
<div class="h5">{{ provides }}</div>
109+
110+
<ul>
111+
{% for link in links %}
112+
{% if link.linkedPackageName is existing_package %}
113+
<li><a href="{{ path('dashboard_packages_info', {packageName: link.linkedPackageName}) }}">{{ link.linkedPackageName }}</a>: {{ link.linkedVersionConstraint }}</li>
114+
{% elseif link.implementation and link.implementedPackageName is existing_package %}
115+
<li><a href="{{ path('dashboard_packages_info', {packageName: link.implementedPackageName}) }}">{{ link.linkedPackageName }}</a>: {{ link.linkedVersionConstraint }}</li>
116+
{% else %}
117+
<li>{{ link.linkedPackageName }}: {{ link.linkedVersionConstraint }}</li>
118+
{% endif %}
119+
{% else %}
120+
<li>None</li>
121+
{% endfor %}
122+
</ul>
123+
</div>
124+
{% endmacro %}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{% extends 'dashboard/packages/package_base.html.twig' %}
2+
3+
{% block page_title %}{{ package.name }} <small>{{ title|trans }}</small>{% endblock %}
4+
5+
{% block page_content %}
6+
{% include 'dashboard/packages/_package_header.html.twig' with {currentPage: title|lower} %}
7+
8+
{{ include('dashboard/packages/_package_list.html.twig', {packages: packageLinks|map(packageLink => packageLink.package), paginator: paginator}, with_context: false) }}
9+
{% endblock %}

tests/FunctionalTests/Controller/Dashboard/DashboardPackagesInfoControllerTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace CodedMonkey\Dirigent\Tests\FunctionalTests\Controller\Dashboard;
44

5+
use CodedMonkey\Dirigent\Doctrine\Entity\User;
56
use CodedMonkey\Dirigent\Doctrine\Repository\PackageRepository;
7+
use CodedMonkey\Dirigent\Doctrine\Repository\UserRepository;
68
use CodedMonkey\Dirigent\Tests\FunctionalTests\WebTestCaseTrait;
79
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
810

@@ -40,6 +42,78 @@ public function testVersions(): void
4042
$this->assertResponseStatusCodeSame(200);
4143
}
4244

45+
public function testDependents(): void
46+
{
47+
$client = static::createClient();
48+
49+
/** @var UserRepository $userRepository */
50+
$userRepository = $client->getContainer()->get(UserRepository::class);
51+
52+
/** @var User $user */
53+
$user = $userRepository->findOneByUsername('user');
54+
$client->loginUser($user);
55+
56+
$client->request('GET', '/packages/psr/log/dependents');
57+
58+
$this->assertResponseStatusCodeSame(200);
59+
60+
$this->assertAnySelectorTextSame('h1 small', 'Dependents');
61+
}
62+
63+
public function testImplementations(): void
64+
{
65+
$client = static::createClient();
66+
67+
/** @var UserRepository $userRepository */
68+
$userRepository = $client->getContainer()->get(UserRepository::class);
69+
70+
/** @var User $user */
71+
$user = $userRepository->findOneByUsername('user');
72+
$client->loginUser($user);
73+
74+
$client->request('GET', '/packages/psr/log/implementations');
75+
76+
$this->assertResponseStatusCodeSame(200);
77+
78+
$this->assertAnySelectorTextSame('h1 small', 'Implementations');
79+
}
80+
81+
public function testProviders(): void
82+
{
83+
$client = static::createClient();
84+
85+
/** @var UserRepository $userRepository */
86+
$userRepository = $client->getContainer()->get(UserRepository::class);
87+
88+
/** @var User $user */
89+
$user = $userRepository->findOneByUsername('user');
90+
$client->loginUser($user);
91+
92+
$client->request('GET', '/packages/psr/log/providers');
93+
94+
$this->assertResponseStatusCodeSame(200);
95+
96+
$this->assertAnySelectorTextSame('h1 small', 'Providers');
97+
}
98+
99+
public function testSuggesters(): void
100+
{
101+
$client = static::createClient();
102+
103+
/** @var UserRepository $userRepository */
104+
$userRepository = $client->getContainer()->get(UserRepository::class);
105+
106+
/** @var User $user */
107+
$user = $userRepository->findOneByUsername('user');
108+
$client->loginUser($user);
109+
110+
$client->request('GET', '/packages/psr/log/suggesters');
111+
112+
$this->assertResponseStatusCodeSame(200);
113+
114+
$this->assertAnySelectorTextSame('h1 small', 'Suggesters');
115+
}
116+
43117
public function testStatistics(): void
44118
{
45119
$client = static::createClient();

translations/messages.en.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ Account: Account
22
Administration: Administration
33
Credits: Credits
44
Dashboard: Dashboard
5-
Dependants: Dependants
5+
Dependents: Dependents
66
Documentation: Documentation
7+
Implementations: Implementations
78
Info: Info
89
Packages: Packages
910
Personal: Personal
11+
Providers: Providers
1012
Sign out: Sign out
1113
Statistics: Statistics
14+
Suggesters: Suggesters
1215
Usage: Usage
1316

1417
Access token: Access token

0 commit comments

Comments
 (0)