Skip to content

Commit 3797fdd

Browse files
committed
Expand CWA profiler panel with four new data categories
Adds four new sections to the Symfony web profiler CWA panel: 1. Publishable ORM queries — per-class draft vs published-only mode decision from PublishableExtension (item and collection queries). 2. PageDataProperty resolutions — outcome of each dynamic component slot resolution in ComponentPositionNormalizer (success or skip reason: no_path / no_pagedata / no_request / property_missing / no_component / not_published / not_in_allowed). 3. Write invalidation fan-out — entity created/updated/deleted counts (from PropagateUpdatesListener) and cache-purged IRI list (from HttpCachePurger). 4. Private Mercure upgrades — topics and resource class whenever PublishableAwareHub upgrades a Mercure update to private:true for an unpublished draft resource. Unit tests extended to cover all new CwaCollectorData recording methods.
1 parent e1334f5 commit 3797fdd

12 files changed

Lines changed: 517 additions & 13 deletions

File tree

src/DataCollector/CwaCollectorData.php

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,28 @@ final class CwaCollectorData implements ResetInterface
3232
private ?string $resolvedRouteIri = null;
3333
private bool $pageDataFound = false;
3434

35-
// --- Mercure ---
35+
// --- Mercure publications ---
3636
/** @var list<string> */
3737
private array $publishedTopics = [];
3838

39+
// --- Publishable ORM queries ---
40+
/** @var list<array{class: string, mode: string, queryType: string}> */
41+
private array $publishableQueries = [];
42+
43+
// --- PageDataProperty resolutions ---
44+
/** @var list<array{property: string, resolvedClass: string|null, skipReason: string|null}> */
45+
private array $pageDataResolutions = [];
46+
47+
// --- Write invalidation fan-out ---
48+
/** @var array{created: int, updated: int, deleted: int} */
49+
private array $invalidationCounts = ['created' => 0, 'updated' => 0, 'deleted' => 0];
50+
/** @var list<string> */
51+
private array $cachePurgedIris = [];
52+
53+
// --- Private Mercure upgrades ---
54+
/** @var list<array{topics: list<string>, resourceClass: string}> */
55+
private array $mercurePrivateUpgrades = [];
56+
3957
// -----------------------------------------------------------------------
4058
// JWT
4159
// -----------------------------------------------------------------------
@@ -107,7 +125,7 @@ public function isPageDataFound(): bool
107125
}
108126

109127
// -----------------------------------------------------------------------
110-
// Mercure
128+
// Mercure publications
111129
// -----------------------------------------------------------------------
112130

113131
public function recordMercurePublication(string $topic): void
@@ -126,6 +144,106 @@ public function getPublishedTopicsCount(): int
126144
return \count($this->publishedTopics);
127145
}
128146

147+
// -----------------------------------------------------------------------
148+
// Publishable ORM queries
149+
// -----------------------------------------------------------------------
150+
151+
public function recordPublishableQuery(string $class, string $mode, string $queryType): void
152+
{
153+
$this->publishableQueries[] = ['class' => $class, 'mode' => $mode, 'queryType' => $queryType];
154+
}
155+
156+
/** @return list<array{class: string, mode: string, queryType: string}> */
157+
public function getPublishableQueries(): array
158+
{
159+
return $this->publishableQueries;
160+
}
161+
162+
public function getPublishableQueryCount(): int
163+
{
164+
return \count($this->publishableQueries);
165+
}
166+
167+
// -----------------------------------------------------------------------
168+
// PageDataProperty resolutions
169+
// -----------------------------------------------------------------------
170+
171+
public function recordPageDataResolution(string $property, ?string $resolvedClass, ?string $skipReason): void
172+
{
173+
$this->pageDataResolutions[] = ['property' => $property, 'resolvedClass' => $resolvedClass, 'skipReason' => $skipReason];
174+
}
175+
176+
/** @return list<array{property: string, resolvedClass: string|null, skipReason: string|null}> */
177+
public function getPageDataResolutions(): array
178+
{
179+
return $this->pageDataResolutions;
180+
}
181+
182+
public function getPageDataResolutionCount(): int
183+
{
184+
return \count($this->pageDataResolutions);
185+
}
186+
187+
// -----------------------------------------------------------------------
188+
// Write invalidation fan-out
189+
// -----------------------------------------------------------------------
190+
191+
public function recordInvalidationCount(string $type): void
192+
{
193+
if (isset($this->invalidationCounts[$type])) {
194+
++$this->invalidationCounts[$type];
195+
}
196+
}
197+
198+
/** @return array{created: int, updated: int, deleted: int} */
199+
public function getInvalidationCounts(): array
200+
{
201+
return $this->invalidationCounts;
202+
}
203+
204+
public function getTotalInvalidated(): int
205+
{
206+
return array_sum($this->invalidationCounts);
207+
}
208+
209+
/** @param list<string> $iris */
210+
public function recordCachePurge(array $iris): void
211+
{
212+
$this->cachePurgedIris = array_values(array_unique(array_merge($this->cachePurgedIris, $iris)));
213+
}
214+
215+
/** @return list<string> */
216+
public function getCachePurgedIris(): array
217+
{
218+
return $this->cachePurgedIris;
219+
}
220+
221+
public function getCachePurgedCount(): int
222+
{
223+
return \count($this->cachePurgedIris);
224+
}
225+
226+
// -----------------------------------------------------------------------
227+
// Private Mercure upgrades
228+
// -----------------------------------------------------------------------
229+
230+
/** @param list<string> $topics */
231+
public function recordMercurePrivateUpgrade(array $topics, string $resourceClass): void
232+
{
233+
$this->mercurePrivateUpgrades[] = ['topics' => $topics, 'resourceClass' => $resourceClass];
234+
}
235+
236+
/** @return list<array{topics: list<string>, resourceClass: string}> */
237+
public function getMercurePrivateUpgrades(): array
238+
{
239+
return $this->mercurePrivateUpgrades;
240+
}
241+
242+
public function getMercurePrivateUpgradeCount(): int
243+
{
244+
return \count($this->mercurePrivateUpgrades);
245+
}
246+
129247
// -----------------------------------------------------------------------
130248
// ResetInterface
131249
// -----------------------------------------------------------------------
@@ -142,5 +260,10 @@ public function reset(): void
142260
$this->pageDataFound = false;
143261

144262
$this->publishedTopics = [];
263+
$this->publishableQueries = [];
264+
$this->pageDataResolutions = [];
265+
$this->invalidationCounts = ['created' => 0, 'updated' => 0, 'deleted' => 0];
266+
$this->cachePurgedIris = [];
267+
$this->mercurePrivateUpgrades = [];
145268
}
146269
}

src/DataCollector/CwaDataCollector.php

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818
/**
1919
* Symfony profiler/toolbar panel for the CWA API Components Bundle.
2020
*
21-
* Shows three categories of per-request information:
22-
* 1. JWT / authentication — cookie presence, refresh, clearance
23-
* 2. Route resolution — path header, resolved route IRI, page data
24-
* 3. Mercure publications — count and topic list
21+
* Shows per-request information across seven categories:
22+
* 1. JWT / authentication — cookie presence, refresh, clearance
23+
* 2. Route resolution — path header, resolved route IRI, page data
24+
* 3. Mercure publications — count and topic list
25+
* 4. Publishable ORM queries — draft vs published-only mode per class
26+
* 5. PageDataProperty resolution — outcome per dynamic slot
27+
* 6. Write invalidation fan-out — entity counts and cache-purged IRIs
28+
* 7. Private Mercure upgrades — topics upgraded to private for draft resources
2529
*
2630
* @author Daniel West <daniel@silverback.is>
2731
*/
@@ -45,9 +49,27 @@ public function collect(Request $request, Response $response, ?\Throwable $excep
4549
'resolved_route_iri' => $this->collectorData->getResolvedRouteIri(),
4650
'page_data_found' => $this->collectorData->isPageDataFound(),
4751

48-
// Mercure
52+
// Mercure publications
4953
'published_topics' => $this->collectorData->getPublishedTopics(),
5054
'published_topics_count' => $this->collectorData->getPublishedTopicsCount(),
55+
56+
// Publishable ORM queries
57+
'publishable_queries' => $this->collectorData->getPublishableQueries(),
58+
'publishable_query_count' => $this->collectorData->getPublishableQueryCount(),
59+
60+
// PageDataProperty resolutions
61+
'page_data_resolutions' => $this->collectorData->getPageDataResolutions(),
62+
'page_data_resolution_count' => $this->collectorData->getPageDataResolutionCount(),
63+
64+
// Write invalidation fan-out
65+
'invalidation_counts' => $this->collectorData->getInvalidationCounts(),
66+
'total_invalidated' => $this->collectorData->getTotalInvalidated(),
67+
'cache_purged_iris' => $this->collectorData->getCachePurgedIris(),
68+
'cache_purged_count' => $this->collectorData->getCachePurgedCount(),
69+
70+
// Private Mercure upgrades
71+
'mercure_private_upgrades' => $this->collectorData->getMercurePrivateUpgrades(),
72+
'mercure_private_upgrade_count' => $this->collectorData->getMercurePrivateUpgradeCount(),
5173
];
5274
}
5375

@@ -111,4 +133,59 @@ public function getPublishedTopicsCount(): int
111133
{
112134
return (int) ($this->data['published_topics_count'] ?? 0);
113135
}
136+
137+
/** @return list<array{class: string, mode: string, queryType: string}> */
138+
public function getPublishableQueries(): array
139+
{
140+
return $this->data['publishable_queries'] ?? [];
141+
}
142+
143+
public function getPublishableQueryCount(): int
144+
{
145+
return (int) ($this->data['publishable_query_count'] ?? 0);
146+
}
147+
148+
/** @return list<array{property: string, resolvedClass: string|null, skipReason: string|null}> */
149+
public function getPageDataResolutions(): array
150+
{
151+
return $this->data['page_data_resolutions'] ?? [];
152+
}
153+
154+
public function getPageDataResolutionCount(): int
155+
{
156+
return (int) ($this->data['page_data_resolution_count'] ?? 0);
157+
}
158+
159+
/** @return array{created: int, updated: int, deleted: int} */
160+
public function getInvalidationCounts(): array
161+
{
162+
return $this->data['invalidation_counts'] ?? ['created' => 0, 'updated' => 0, 'deleted' => 0];
163+
}
164+
165+
public function getTotalInvalidated(): int
166+
{
167+
return (int) ($this->data['total_invalidated'] ?? 0);
168+
}
169+
170+
/** @return list<string> */
171+
public function getCachePurgedIris(): array
172+
{
173+
return $this->data['cache_purged_iris'] ?? [];
174+
}
175+
176+
public function getCachePurgedCount(): int
177+
{
178+
return (int) ($this->data['cache_purged_count'] ?? 0);
179+
}
180+
181+
/** @return list<array{topics: list<string>, resourceClass: string}> */
182+
public function getMercurePrivateUpgrades(): array
183+
{
184+
return $this->data['mercure_private_upgrades'] ?? [];
185+
}
186+
187+
public function getMercurePrivateUpgradeCount(): int
188+
{
189+
return (int) ($this->data['mercure_private_upgrade_count'] ?? 0);
190+
}
114191
}

src/Doctrine/Extension/ORM/PublishableExtension.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Doctrine\ORM\QueryBuilder;
2121
use Doctrine\Persistence\ManagerRegistry;
2222
use Silverback\ApiComponentsBundle\Annotation\Publishable;
23+
use Silverback\ApiComponentsBundle\DataCollector\CwaCollectorData;
2324
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
2425
use Symfony\Component\HttpFoundation\RequestStack;
2526

@@ -32,7 +33,7 @@ final class PublishableExtension implements QueryItemExtensionInterface, QueryCo
3233
private RequestStack $requestStack;
3334
private ManagerRegistry $registry;
3435

35-
public function __construct(PublishableStatusChecker $publishableStatusChecker, RequestStack $requestStack, ManagerRegistry $registry)
36+
public function __construct(PublishableStatusChecker $publishableStatusChecker, RequestStack $requestStack, ManagerRegistry $registry, private readonly ?CwaCollectorData $collectorData = null)
3637
{
3738
$this->publishableStatusChecker = $publishableStatusChecker;
3839
$this->requestStack = $requestStack;
@@ -50,10 +51,12 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
5051
if (!$this->isDraftRequest($resourceClass, $context)) {
5152
// User has no access to draft object
5253
$this->updateQueryBuilderForUnauthorizedUsers($queryBuilder, $configuration);
54+
$this->collectorData?->recordPublishableQuery($resourceClass, 'published_only', 'item');
5355

5456
return;
5557
}
5658

59+
$this->collectorData?->recordPublishableQuery($resourceClass, 'draft', 'item');
5760
$alias = $queryBuilder->getRootAliases()[0];
5861
$classMetadata = $this->registry->getManagerForClass($resourceClass)->getClassMetadata($resourceClass);
5962

@@ -89,10 +92,13 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
8992
if (!$this->isDraftRequest($resourceClass, $context)) {
9093
// User has no access to draft object
9194
$this->updateQueryBuilderForUnauthorizedUsers($queryBuilder, $configuration);
95+
$this->collectorData?->recordPublishableQuery($resourceClass, 'published_only', 'collection');
9296

9397
return;
9498
}
9599

100+
$this->collectorData?->recordPublishableQuery($resourceClass, 'draft', 'collection');
101+
96102
$alias = $queryBuilder->getRootAliases()[0];
97103
$identifiers = $this->registry->getManagerForClass($resourceClass)->getClassMetadata($resourceClass)->getIdentifier();
98104
$dql = $this->getDQL($configuration, $resourceClass);

src/EventListener/Doctrine/PropagateUpdatesListener.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Doctrine\Persistence\ObjectManager;
2626
use Doctrine\Persistence\ObjectRepository;
2727
use Doctrine\Persistence\Proxy;
28+
use Silverback\ApiComponentsBundle\DataCollector\CwaCollectorData;
2829
use Silverback\ApiComponentsBundle\DataProvider\PageDataProvider;
2930
use Silverback\ApiComponentsBundle\Entity\Component\Collection;
3031
use Silverback\ApiComponentsBundle\Entity\Core\PageDataInterface;
@@ -52,6 +53,7 @@ public function __construct(
5253
private readonly ResourceClassResolverInterface $resourceClassResolver,
5354
private readonly PageDataProvider $pageDataProvider,
5455
private readonly ComponentPositionRepository $positionRepository,
56+
private readonly ?CwaCollectorData $collectorData = null,
5557
) {
5658
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
5759
$this->collectionRepository = $entityManager->getRepository(Collection::class);
@@ -244,6 +246,7 @@ private function addResourceIrisFromObject($resource, string $type): void
244246
'type' => $type,
245247
'resourceClass' => $resourceClass,
246248
];
249+
$this->collectorData?->recordInvalidationCount($type);
247250
}
248251

249252
private function collectDynamicComponentPositionResources(?array $pageDataPropertiesChanged = null): void

src/HttpCache/HttpCachePurger.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2121
use ApiPlatform\Metadata\UrlGeneratorInterface;
2222
use Doctrine\ORM\PersistentCollection;
23+
use Silverback\ApiComponentsBundle\DataCollector\CwaCollectorData;
2324

2425
class HttpCachePurger implements ResourceChangedPropagatorInterface
2526
{
@@ -29,6 +30,7 @@ public function __construct(
2930
private readonly IriConverterInterface $iriConverter,
3031
private readonly ResourceClassResolverInterface $resourceClassResolver,
3132
private readonly ?PurgerInterface $httpCachePurger,
33+
private readonly ?CwaCollectorData $collectorData = null,
3234
) {
3335
$this->reset();
3436
}
@@ -91,7 +93,9 @@ public function propagate(): void
9193
return;
9294
}
9395

94-
$this->httpCachePurger && $this->httpCachePurger->purge(array_values($this->tags));
96+
$iris = array_values($this->tags);
97+
$this->collectorData?->recordCachePurge($iris);
98+
$this->httpCachePurger && $this->httpCachePurger->purge($iris);
9599
$this->reset();
96100
}
97101

src/Mercure/PublishableAwareHub.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
1515
use ApiPlatform\Metadata\IriConverterInterface;
16+
use Silverback\ApiComponentsBundle\DataCollector\CwaCollectorData;
1617
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
1718
use Symfony\Component\Mercure\HubInterface;
1819
use Symfony\Component\Mercure\Jwt\TokenFactoryInterface;
@@ -24,7 +25,7 @@
2425
*/
2526
class PublishableAwareHub implements HubInterface
2627
{
27-
public function __construct(private HubInterface $decorated, private PublishableStatusChecker $publishableStatusChecker, private IriConverterInterface $iriConverter)
28+
public function __construct(private HubInterface $decorated, private PublishableStatusChecker $publishableStatusChecker, private IriConverterInterface $iriConverter, private readonly ?CwaCollectorData $collectorData = null)
2829
{
2930
}
3031

@@ -59,6 +60,7 @@ public function publish(Update $update): string
5960

6061
if ($this->publishableStatusChecker->getAttributeReader()->isConfigured($resource) && !$this->publishableStatusChecker->isActivePublishedAt($resource)) {
6162
$update = new Update(topics: $update->getTopics(), data: $update->getData(), private: true, id: $update->getId(), type: $update->getType(), retry: $update->getRetry());
63+
$this->collectorData?->recordMercurePrivateUpgrade($update->getTopics(), $resource::class);
6264
}
6365
}
6466

0 commit comments

Comments
 (0)