Skip to content

Commit

Permalink
Implement PWA Collector and additional viewing templates
Browse files Browse the repository at this point in the history
Implemented a new Progressive Web App (PWA) Collector to fetch and manage service worker and manifest details. Additionally, created multiple HTML templates for manifest, service worker, and other PWA tabs to properly display the collected data in UI.
  • Loading branch information
Spomky committed Apr 5, 2024
1 parent e518dac commit 7a5ed20
Show file tree
Hide file tree
Showing 6 changed files with 555 additions and 2 deletions.
121 changes: 121 additions & 0 deletions src/DataCollector/PwaCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\DataCollector;

use SpomkyLabs\PwaBundle\CachingStrategy\CacheStrategyInterface;
use SpomkyLabs\PwaBundle\CachingStrategy\HasCacheStrategiesInterface;
use SpomkyLabs\PwaBundle\Dto\Manifest;
use SpomkyLabs\PwaBundle\Dto\ServiceWorker;
use SpomkyLabs\PwaBundle\Dto\Workbox;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Throwable;
use function count;
use function in_array;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;

final class PwaCollector extends DataCollector
{
/**
* @param iterable<HasCacheStrategiesInterface> $cachingServices
*/
public function __construct(
private readonly SerializerInterface $serializer,
#[TaggedIterator('spomky_labs_pwa.cache_strategy')]
private readonly iterable $cachingServices,
private readonly Manifest $manifest,
private readonly ServiceWorker $serviceWorker,
#[Autowire(param: 'spomky_labs_pwa.manifest.enabled')]
private readonly bool $manifestEnabled,
) {
}

public function collect(Request $request, Response $response, Throwable $exception = null): void
{
$jsonOptions = [
AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => true,
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
JsonEncode::OPTIONS => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT,
];
$this->data['cachingStrategies'] = [];
foreach ($this->cachingServices as $cachingService) {
foreach ($cachingService->getCacheStrategies() as $cacheStrategy) {
$this->data['cachingStrategies'][] = $cacheStrategy;
}
}
$this->data['serviceWorker'] = $this->serviceWorker;
$this->data['manifest'] = [
'enabled' => $this->manifestEnabled,
'data' => $this->manifest,
'installable' => $this->isInstallable(),
'output' => $this->serializer->serialize($this->manifest, 'json', $jsonOptions),
];

dump($this->data);
}

public function getData(): array

Check failure on line 68 in src/DataCollector/PwaCollector.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test on ubuntu-latest

Method SpomkyLabs\PwaBundle\DataCollector\PwaCollector::getData() return type has no value type specified in iterable type array.
{
return $this->data;

Check failure on line 70 in src/DataCollector/PwaCollector.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test on ubuntu-latest

Method SpomkyLabs\PwaBundle\DataCollector\PwaCollector::getData() should return array but returns array|Symfony\Component\VarDumper\Cloner\Data.
}

/**
* @return array<CacheStrategyInterface>
*/
public function getCachingStrategies(): array
{
return $this->data['cachingStrategies'] ?? [];
}

public function getManifest(): Manifest
{
return $this->data['manifest']['data'];
}

public function getWorkbox(): Workbox
{
return $this->data['serviceWorker']->workbox;
}

public function getName(): string
{
return 'pwa';
}

/**
* @return array{status: bool, reasons: string[]}
*/
private function isInstallable(): array
{
$reasons = [
'The manifest must be enabled' => ! $this->manifestEnabled,
'The manifest must have a short name or a name' => $this->manifest->shortName === null && $this->manifest->name === null,
'The manifest must have a start URL' => $this->manifest->startUrl === null,
'The manifest must have a display value set to "standalone", "fullscreen" or "minimal-ui' => ! in_array(
$this->manifest->display,
['standalone', 'fullscreen', 'minimal-ui'],
true
),
'The manifest must have at least one icon' => $this->manifest->icons === null || count(

Check failure on line 110 in src/DataCollector/PwaCollector.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test on ubuntu-latest

Strict comparison using === between array and null will always evaluate to false.
$this->manifest->icons
) === 0,
'The manifest must have the "prefer_related_applications" property set to a value other than "true"' => $this->manifest->preferRelatedApplications === true,
];

return [

Check failure on line 116 in src/DataCollector/PwaCollector.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test on ubuntu-latest

Method SpomkyLabs\PwaBundle\DataCollector\PwaCollector::isInstallable() should return array{status: bool, reasons: array<string>} but returns array{status: bool, reasons: array{The manifest must be enabled: bool, The manifest must have a short name or a name: bool, The manifest must have a start URL: bool, 'The manifest must have a display value set to "standalone", "fullscreen" or "minimal-ui': bool, The manifest must have at least one icon: bool, 'The manifest must have the "prefer_related_applications" property set to a value other than "true"': bool}}.
'status' => count(array_filter($reasons)) === 0,
'reasons' => $reasons,
];
}
}
14 changes: 12 additions & 2 deletions src/Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use SpomkyLabs\PwaBundle\Command\CreateIconsCommand;
use SpomkyLabs\PwaBundle\Command\CreateScreenshotCommand;
use SpomkyLabs\PwaBundle\Command\ListCacheStrategiesCommand;
use SpomkyLabs\PwaBundle\DataCollector\PwaCollector;
use SpomkyLabs\PwaBundle\Dto\Manifest;
use SpomkyLabs\PwaBundle\Dto\ServiceWorker;
use SpomkyLabs\PwaBundle\ImageProcessor\GDImageProcessor;
Expand All @@ -28,8 +29,8 @@
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;

return static function (ContainerConfigurator $container): void {
$container = $container->services()
return static function (ContainerConfigurator $configurator): void {
$container = $configurator->services()
->defaults()
->private()
->autoconfigure()
Expand Down Expand Up @@ -124,4 +125,13 @@
->tag('spomky_labs_pwa.match_callback_handler')
;
$container->load('SpomkyLabs\\PwaBundle\\MatchCallbackHandler\\', '../../MatchCallbackHandler/*');

if ($configurator->env() !== 'prod') {
$container->set(PwaCollector::class)
->tag('data_collector', [
'template' => '@SpomkyLabsPwa/Collector/template.html.twig',
'id' => 'pwa',
])
;
}
};
170 changes: 170 additions & 0 deletions templates/Collector/manifest-tab.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<h3>General information</h3>
<p>
Status:
{% if collector.data.manifest.enabled %}
<span class="status-badge status-success">enabled</span>
{% else %}
<span class="status-badge status-warning">disabled</span>
{% endif %}
<br>
Can be installed:
{% if collector.data.manifest.installable.status %}
<span class="status-badge status-success">yes</span>
{% else %}
<span class="status-badge status-warning">no</span>
{% endif %}
</p>
<ul>
{% for reason, value in collector.data.manifest.installable.reasons %}
<li>
{% if not value %}
<span class="badge status-success">success</span>
{% else %}
<span class="badge status-error">failure</span>
{% endif %}:
{{ reason }}
</li>
{% endfor %}
</ul>
<h3>Details</h3>
<table>
<thead>
<tr>
<th scope="col" class="key">Key</th>
<th scope="col">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>ID</td>
<td>{{ collector.getManifest().id }}</td>
</tr>
<tr>
<td>Name</td>
<td>{{ collector.getManifest().name|trans }}</td>
</tr>
<tr>
<td>Short name</td>
<td>{{ collector.getManifest().shortName|trans }}</td>
</tr>
<tr>
<td>Description name</td>
<td>{{ collector.getManifest().description|trans }}</td>
</tr>
<tr>
<td>Theme color</td>
<td>
{% if collector.getManifest().themeColor %}
{{ collector.getManifest().themeColor }}
<span style="background-color: {{ collector.getManifest().themeColor }}; padding: 0.25rem; color: {{ collector.getManifest().themeColor }};">{{ collector.getManifest().themeColor }}</span>
{% else %}
n/a
{% endif %}
</td>
</tr>
<tr>
<td>Background color</td>
<td>
{% if collector.getManifest().backgroundColor %}
{{ collector.getManifest().backgroundColor }}
<span style="background-color: {{ collector.getManifest().backgroundColor }}; padding: 0.25rem; color: {{ collector.getManifest().backgroundColor }};">{{ collector.getManifest().backgroundColor }}</span>
{% else %}
n/a
{% endif %}
</td>
</tr>
<tr>
<td>Display</td>
<td>{{ collector.getManifest().display }}</td>
</tr>
<tr>
<td>Orientation</td>
<td>{{ collector.getManifest().orientation }}</td>
</tr>
<tr>
<td>Scope</td>
<td>{{ collector.getManifest().scope }}</td>
</tr>
<tr>
<td>Start URL</td>
<td>{{ collector.getManifest().startUrl }}</td>
</tr>
<tr>
<td>Categories</td>
<td>
{% if collector.getManifest().categories|length == 0 %}
<span class="badge">none</span>
{% else %}
<ul>
{% for category in collector.getManifest().categories %}
<li>{{ category|trans }}</li>
{% endfor %}
</ul>
{% endif %}
</td>
</tr>
</tbody>
</table>
<h3 class="tab-title">Application Icons</h3>
<div class="tab-content">
<table>
<thead>
<tr>
<th scope="col" class="key">Source</th>
<th scope="col">Sizes</th>
<th scope="col">Type</th>
<th scope="col">Purpose</th>
</tr>
</thead>
<tbody>
{% for icon in collector.getManifest().icons %}
<tr>
<td>{{ icon.src.src }}</td>
<td>{{ icon.getSizeList() }}</td>
<td>{{ icon.type|default('n/a') }}</td>
<td>{{ icon.purpose|default('n/a') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<h3 class="tab-title">Application screenshots</h3>
<div class="tab-content">
<table>
<thead>
<tr>
<th scope="col" class="key">Source</th>
<th scope="col">Size</th>
<th scope="col">Type</th>
<th scope="col">Form factor</th>
<th scope="col">Label</th>
<th scope="col">Platform</th>
</tr>
</thead>
<tbody>
{% for screenshot in collector.getManifest().screenshots %}
<tr>
<td>
{{ screenshot.src.src }}<br>
<span class="badge">{{ screenshot.reference|default('No reference') }}</span>
</td>
<td>
{% if screenshot.height and screenshot.width %}
{{ screenshot.width }}x{{ screenshot.height }}
{% else %}
n/a
{% endif %}
</td>
<td>{{ screenshot.type|default('n/a') }}</td>
<td>{{ screenshot.formFactor|default('n/a') }}</td>
<td>{{ screenshot.label|default('n/a') }}</td>
<td>{{ screenshot.platform|default('n/a') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<h3 class="tab-title">Output</h3>
<div class="tab-content">
<pre class="output">{{ collector.data.manifest.output|nl2br }}</pre>
</div>
12 changes: 12 additions & 0 deletions templates/Collector/pwa.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 7a5ed20

Please sign in to comment.