Skip to content

Commit 1ce7c26

Browse files
committed
Create package link pages
1 parent a680986 commit 1ce7c26

File tree

9 files changed

+244
-20
lines changed

9 files changed

+244
-20
lines changed

src/Controller/Dashboard/DashboardPackagesController.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ public function __construct(
3535
public function list(Request $request): Response
3636
{
3737
$queryBuilder = $this->packageRepository->createQueryBuilder('package');
38-
$queryBuilder->addOrderBy('package.name', 'ASC');
3938

4039
if (null !== $query = $request->query->get('query')) {
4140
$queryBuilder->andWhere($queryBuilder->expr()->like('package.name', ':query'));

src/Controller/Dashboard/DashboardPackagesInfoController.php

Lines changed: 93 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,80 @@ 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, PackageRequireLink::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+
99+
return $this->packageLinks($request, $package, PackageProvideLink::class, 'Implementations', queryBuilder: $queryBuilder);
100+
}
101+
102+
#[Route('/packages/{packageName}/providers', name: 'dashboard_packages_providers', requirements: ['packageName' => '[a-z0-9_.-]+/[a-z0-9_.-]+'])]
103+
#[IsGrantedAccess]
104+
public function providers(Request $request, string $packageName): Response
105+
{
106+
$package = $this->packageRepository->findOneBy(['name' => $packageName]);
107+
108+
$providerRepository = $this->entityManager->getRepository(PackageProvideLink::class);
109+
$queryBuilder = $providerRepository->createQueryBuilder('provider');
110+
$queryBuilder
111+
->leftJoin('provider.package', 'package')
112+
->andWhere('provider.linkedPackageName = :packageName')
113+
->andWhere('provider.implementation = false')
114+
->setParameter('packageName', $package->getName());
115+
116+
return $this->packageLinks($request, $package, PackageProvideLink::class, 'Providers', queryBuilder: $queryBuilder);
117+
}
118+
119+
#[Route('/packages/{packageName}/suggesters', name: 'dashboard_packages_suggesters', requirements: ['packageName' => '[a-z0-9_.-]+/[a-z0-9_.-]+'])]
120+
#[IsGrantedAccess]
121+
public function suggesters(Request $request, string $packageName): Response
122+
{
123+
$package = $this->packageRepository->findOneBy(['name' => $packageName]);
124+
125+
return $this->packageLinks($request, $package, PackageSuggestLink::class, 'Suggesters');
126+
}
127+
128+
private function packageLinks(Request $request, Package $package, string $linkClass, string $title, ?QueryBuilder $queryBuilder = null): Response
129+
{
130+
if (!$queryBuilder) {
131+
$dependentRepository = $this->entityManager->getRepository($linkClass);
132+
$queryBuilder = $dependentRepository->createQueryBuilder('link');
133+
$queryBuilder
134+
->leftJoin('link.package', 'package')
135+
->andWhere('link.linkedPackageName = :packageName')
136+
->setParameter('packageName', $package->getName());
137+
}
138+
139+
$paginator = PackagePaginator::fromRequest($request, $queryBuilder, $this->container->get('router'));
140+
$packageLinks = $paginator->getResults();
141+
142+
return $this->render('dashboard/packages/package_links.html.twig', [
143+
'package' => $package,
144+
'packageLinks' => $packageLinks,
145+
'paginator' => $paginator,
146+
'title' => $title,
147+
]);
148+
}
149+
61150
#[Route('/packages/{packageName}/statistics', name: 'dashboard_packages_statistics', requirements: ['packageName' => '[a-z0-9_.-]+/[a-z0-9_.-]+'])]
62151
#[IsGrantedAccess]
63152
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
}

src/EasyAdmin/PackagePaginator.php

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace CodedMonkey\Dirigent\EasyAdmin;
44

55
use Doctrine\ORM\QueryBuilder;
6-
use Doctrine\ORM\Tools\Pagination\Paginator;
76
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Orm\EntityPaginatorInterface;
87
use EasyCorp\Bundle\EasyAdminBundle\Dto\PaginatorDto;
98
use Symfony\Component\HttpFoundation\Request;
@@ -37,15 +36,27 @@ public function paginate(PaginatorDto $paginatorDto, QueryBuilder $queryBuilder)
3736
$this->rangeFirstResultNumber = $this->pageSize * ($this->currentPage - 1) + 1;
3837
$this->rangeLastResultNumber = $this->rangeFirstResultNumber + $this->pageSize - 1;
3938

40-
$query = $queryBuilder
39+
$countQueryBuilder = clone $queryBuilder;
40+
$this->numResults = $countQueryBuilder
41+
->select('COUNT(package.id)')
42+
->getQuery()
43+
->getSingleScalarResult();
44+
45+
if ($this->rangeFirstResultNumber > $this->numResults) {
46+
$this->results = [];
47+
$this->rangeLastResultNumber = $this->numResults;
48+
49+
return $this;
50+
}
51+
52+
$results = $queryBuilder
53+
->addOrderBy('package.name', 'ASC')
4154
->setFirstResult($firstResult)
4255
->setMaxResults($this->pageSize)
43-
->getQuery();
44-
45-
$paginator = new Paginator($query, $paginatorDto->fetchJoinCollection());
56+
->getQuery()
57+
->getResult();
4658

47-
$this->results = $paginator->getIterator();
48-
$this->numResults = $paginator->count();
59+
$this->results = $results;
4960
if ($this->rangeLastResultNumber > $this->numResults) {
5061
$this->rangeLastResultNumber = $this->numResults;
5162
}
@@ -55,15 +66,15 @@ public function paginate(PaginatorDto $paginatorDto, QueryBuilder $queryBuilder)
5566

5667
public static function fromRequest(Request $request, QueryBuilder $queryBuilder, UrlGeneratorInterface $router): EntityPaginatorInterface
5768
{
58-
$paginatorDto = new PaginatorDto(20, 3, 1, true, null);
59-
$paginatorDto->setPageNumber($request->query->getInt('page', 1));
60-
6169
$paginator = new self(
6270
$router,
6371
$request->attributes->get('_route'),
6472
$request->attributes->get('_route_params'),
6573
);
6674

75+
$paginatorDto = new PaginatorDto(20, 3, 1, true, null);
76+
$paginatorDto->setPageNumber($request->query->getInt('page', 1));
77+
6778
return $paginator->paginate($paginatorDto, $queryBuilder);
6879
}
6980

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();

0 commit comments

Comments
 (0)