From fccd95918c2f3eee63a39cd8d3d7dc24f5a55473 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sun, 29 Sep 2024 11:53:03 +0200 Subject: [PATCH] Add new Stimulus controllers and improve script environment handling Introduced multiple new Stimulus controllers for handling various browser features such as battery, device orientation, geolocation, and more. Converted environment handling in `castor.php` to use context-based approach, ensuring better isolation and maintainability. --- .github/workflows/integrate.yml | 2 +- assets/package.json | 82 +++++++++++++++++-- assets/src/abstract_controller.js | 10 +++ assets/src/backgroundsync-form_controller.js | 21 +++-- assets/src/badge_controller.js | 16 ++++ assets/src/battery_controller.js | 36 ++++++++ assets/src/connection-status_controller.js | 7 +- assets/src/device-orientation_controller.js | 21 +++++ assets/src/fullscreen_controller.js | 39 +++++++++ assets/src/geolocation_controller.js | 47 +++++++++++ assets/src/install_controller.js | 42 ++++++++++ assets/src/prefetch-on-demand_controller.js | 8 +- assets/src/presentation_controller.js | 69 ++++++++++++++++ assets/src/receiver_controller.js | 27 ++++++ assets/src/share_controller.js | 24 ++++++ assets/src/sync-broadcast_controller.js | 8 +- assets/src/vibration_controller.js | 45 ++++++++++ castor.php | 73 ++++++++--------- link | 0 phpstan-baseline.neon | 5 -- phpstan.neon | 4 +- src/CachingStrategy/AssetCache.php | 1 + src/CachingStrategy/ImageCache.php | 1 + src/CachingStrategy/ManifestCache.php | 1 + .../PreloadUrlsGeneratorManager.php | 1 + src/CachingStrategy/ResourceCaches.php | 1 + src/CachingStrategy/WorkboxCacheStrategy.php | 1 + src/Command/CreateIconsCommand.php | 1 + src/Command/CreateScreenshotCommand.php | 3 +- src/CompilerPass/PreloadUrlCompilerPass.php | 1 + src/ImageProcessor/Configuration.php | 1 + src/ImageProcessor/GDImageProcessor.php | 12 +-- .../DestinationMatchCallbackHandler.php | 1 + .../ExactPathnameMatchCallbackHandler.php | 1 + .../OriginMatchCallbackHandler.php | 1 + .../PathnameEndsWithMatchCallbackHandler.php | 1 + ...PathnameStartsWithMatchCallbackHandler.php | 1 + .../RouteMatchCallbackHandler.php | 1 + src/Normalizer/ScreenshotNormalizer.php | 1 + src/Service/FaviconsCompiler.php | 1 + src/Service/IconResolver.php | 1 + src/Service/ServiceWorkerCompiler.php | 1 + .../AppendCacheStrategies.php | 1 + src/Twig/PwaRuntime.php | 1 + src/WorkboxPlugin/BroadcastUpdatePlugin.php | 2 + src/WorkboxPlugin/CacheableResponsePlugin.php | 2 + src/WorkboxPlugin/ExpirationPlugin.php | 2 + tests/Functional/AbstractPwaTestCase.php | 1 + tests/Functional/GenerateIconsCommandTest.php | 1 + .../Functional/TakeScreenshotCommandTest.php | 1 + tests/TestFilesystem.php | 1 + 51 files changed, 547 insertions(+), 85 deletions(-) create mode 100644 assets/src/abstract_controller.js create mode 100644 assets/src/badge_controller.js create mode 100644 assets/src/battery_controller.js create mode 100644 assets/src/device-orientation_controller.js create mode 100644 assets/src/fullscreen_controller.js create mode 100644 assets/src/geolocation_controller.js create mode 100644 assets/src/install_controller.js create mode 100644 assets/src/presentation_controller.js create mode 100644 assets/src/receiver_controller.js create mode 100644 assets/src/share_controller.js create mode 100644 assets/src/vibration_controller.js mode change 100644 => 100755 link diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index 499a5d3..c014f30 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -16,7 +16,7 @@ jobs: - name: "Check file permissions" run: | - test "$(find . -type f -not -path './.git/*' -executable)" == "" + test "$(find . -type f -not -path './.git/*' -not -wholename './link' -executable)" == "" - name: "Find non-printable ASCII characters" run: | diff --git a/assets/package.json b/assets/package.json index 013a877..58a9e7e 100644 --- a/assets/package.json +++ b/assets/package.json @@ -5,6 +5,27 @@ "version": "1.0.0", "symfony": { "controllers": { + "backgroundsync-form": { + "main": "src/backgroundsync-form_controller.js", + "name": "pwa/backgroundsync-form", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true + }, + "badge": { + "main": "src/badge_controller.js", + "name": "pwa/badge", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true + }, + "battery": { + "main": "src/battery_controller.js", + "name": "pwa/battery", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true + }, "connection-status": { "main": "src/connection-status_controller.js", "name": "pwa/connection-status", @@ -12,16 +33,30 @@ "fetch": "eager", "enabled": true }, - "backgroundsync-form": { - "main": "src/backgroundsync-form_controller.js", - "name": "pwa/backgroundsync-form", + "device-orientation": { + "main": "src/device-orientation_controller.js", + "name": "pwa/device-orientation", "webpackMode": "eager", "fetch": "eager", "enabled": true }, - "sync-broadcast": { - "main": "src/sync-broadcast_controller.js", - "name": "pwa/sync-broadcast", + "fullscreen": { + "main": "src/fullscreen_controller.js", + "name": "pwa/fullscreen", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true + }, + "geolocation": { + "main": "src/geolocation_controller.js", + "name": "pwa/geolocation", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true + }, + "install": { + "main": "src/install_controller.js", + "name": "pwa/install", "webpackMode": "eager", "fetch": "eager", "enabled": true @@ -32,6 +67,41 @@ "webpackMode": "eager", "fetch": "eager", "enabled": true + }, + "presentation": { + "main": "src/presentation_controller.js", + "name": "pwa/presentation", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true + }, + "receiver": { + "main": "src/receiver_controller.js", + "name": "pwa/receiver", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true + }, + "share": { + "main": "src/share_controller.js", + "name": "pwa/share", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true + }, + "sync-broadcast": { + "main": "src/sync-broadcast_controller.js", + "name": "pwa/sync-broadcast", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true + }, + "vibration": { + "main": "src/vibration_controller.js", + "name": "pwa/vibration", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true } }, "importmap": { diff --git a/assets/src/abstract_controller.js b/assets/src/abstract_controller.js new file mode 100644 index 0000000..bab060b --- /dev/null +++ b/assets/src/abstract_controller.js @@ -0,0 +1,10 @@ +'use strict'; + +import { Controller } from '@hotwired/stimulus'; + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + dispatchEvent = (name, payload) => { + this.dispatch(name, { detail: payload }); + } +} diff --git a/assets/src/backgroundsync-form_controller.js b/assets/src/backgroundsync-form_controller.js index 02a9874..ac47b58 100644 --- a/assets/src/backgroundsync-form_controller.js +++ b/assets/src/backgroundsync-form_controller.js @@ -5,13 +5,16 @@ import { Controller } from '@hotwired/stimulus'; /* stimulusFetch: 'lazy' */ export default class extends Controller { static values = { - params: { type: Object, default: { - mode: 'cors', - cache: 'no-cache', - credentials: 'same-origin', - redirect: 'follow', - referrerPolicy: 'no-referrer' - }}, + params: { + type: Object, + default: { + mode: 'cors', + cache: 'no-cache', + credentials: 'same-origin', + redirect: 'follow', + referrerPolicy: 'no-referrer' + } + }, headers: { type: Object, default: {} }, redirection: { type: String, default: null }, }; @@ -36,11 +39,11 @@ export default class extends Controller { params.headers['Content-Type'] = 'application/x-www-form-urlencoded'; params.body = new URLSearchParams(new FormData(form)); } else { - console.error('Unsupported form enctype'); + // Unsupported form enctype + return; } params.method = form.method.toUpperCase(); const response = await fetch(url, params); - console.log(new URLSearchParams(params.body).toString(), params, params.headers); if (response.redirected) { window.location.assign(response.url); return; diff --git a/assets/src/badge_controller.js b/assets/src/badge_controller.js new file mode 100644 index 0000000..efc3ab0 --- /dev/null +++ b/assets/src/badge_controller.js @@ -0,0 +1,16 @@ +'use strict'; + +import AbstractController from './abstract_controller.js'; + +/* stimulusFetch: 'lazy' */ +export default class extends AbstractController { + update = async ({counter}) => { + await navigator.setAppBadge(counter); + this.dispatchEvent('badge:updated', { counter }); + } + + clear = async () => { + await navigator.clearAppBadge(); + this.dispatchEvent('badge:cleared'); + } +} diff --git a/assets/src/battery_controller.js b/assets/src/battery_controller.js new file mode 100644 index 0000000..13e6c8d --- /dev/null +++ b/assets/src/battery_controller.js @@ -0,0 +1,36 @@ +'use strict'; + +import AbstractController from './abstract_controller.js'; + +/* stimulusFetch: 'lazy' */ +export default class extends AbstractController { + async connect() { + const battery = await navigator.getBattery(); + battery.addEventListener('chargingchange', () => this.updateChargeInfo(battery)); + battery.addEventListener('levelchange', () => this.updateLevelInfo(battery)); + battery.addEventListener('chargingtimechange', () => this.updateChargingInfo(battery)); + battery.addEventListener('dischargingtimechange', () => this.updateDischargingInfo(battery)); + + await this.updateChargeInfo(battery); + await this.updateLevelInfo(battery); + await this.updateChargingInfo(battery); + await this.updateDischargingInfo(battery); + } + update = async ({counter}) => { + await navigator.setAppBadge(counter); + this.dispatchEvent('badge:updated', { counter }); + } + + updateChargeInfo = async (battery) => { + this.dispatchEvent('battery:charge', { charging: battery.charging }); + } + updateLevelInfo = async (battery) => { + this.dispatchEvent('battery:level', { level: battery.level }); + } + updateChargingInfo = async (battery) => { + this.dispatchEvent('battery:chargingtime', { chargingTime: battery.chargingTime }); + } + updateDischargingInfo = async (battery) => { + this.dispatchEvent('battery:dischargingtime', { dischargingTime: battery.dischargingTime }); + } +} diff --git a/assets/src/connection-status_controller.js b/assets/src/connection-status_controller.js index a6ee963..ba28ba3 100644 --- a/assets/src/connection-status_controller.js +++ b/assets/src/connection-status_controller.js @@ -1,9 +1,9 @@ 'use strict'; -import { Controller } from '@hotwired/stimulus'; +import AbstractController from './abstract_controller.js'; /* stimulusFetch: 'lazy' */ -export default class extends Controller { +export default class extends AbstractController { static targets = ['message', 'attribute']; static values = { onlineMessage: { type: String, default: 'You are online.' }, @@ -37,9 +37,6 @@ export default class extends Controller { }); }); } - dispatchEvent = (name, payload) => { - this.dispatch(name, { detail: payload }); - } statusChanged = (data) => { this.messageTargets.forEach((element) => { diff --git a/assets/src/device-orientation_controller.js b/assets/src/device-orientation_controller.js new file mode 100644 index 0000000..3a90092 --- /dev/null +++ b/assets/src/device-orientation_controller.js @@ -0,0 +1,21 @@ +'use strict'; + +import AbstractController from './abstract_controller.js'; + +/* stimulusFetch: 'lazy' */ +export default class extends AbstractController { + connect() { + window.addEventListener( + 'deviceorientation', + (event) => { + this.dispatchEvent({ + absolute: event.absolute, + alpha: event.alpha, + beta: event.beta, + gamma: event.gamma + }) + }, + true + ); + } +} diff --git a/assets/src/fullscreen_controller.js b/assets/src/fullscreen_controller.js new file mode 100644 index 0000000..1fc2db9 --- /dev/null +++ b/assets/src/fullscreen_controller.js @@ -0,0 +1,39 @@ +'use strict'; + +import AbstractController from './abstract_controller.js'; + +/* stimulusFetch: 'lazy' */ +export default class extends AbstractController { + connect () { + document.addEventListener("fullscreenchange", () => { + this.dispatchEvent('fullscreen:change', { + fullscreen: document.fullscreenElement !== null, + element: document.fullscreenElement + }); + }); + document.addEventListener("fullscreenerror", () => { + this.dispatchEvent('fullscreen:error', { + element: document.fullscreenElement + }); + }); + } + + request = async (event) => { + const {params} = event; + const {target, ...rest} = params; + if (!target) { + await document.documentElement.requestFullscreen(rest); + return + } + const element = document.getElementById(target); + if (!element) { + console.error('Element not found:', target); + return; + } + await element.requestFullscreen(rest); + } + + exit = async () => { + await document.exitFullscreen(); + } +} diff --git a/assets/src/geolocation_controller.js b/assets/src/geolocation_controller.js new file mode 100644 index 0000000..540eb50 --- /dev/null +++ b/assets/src/geolocation_controller.js @@ -0,0 +1,47 @@ +'use strict'; + +import AbstractController from './abstract_controller.js'; + +/* stimulusFetch: 'lazy' */ +export default class extends AbstractController { + watchId = null; + + locate({params}) { + if (!navigator.geolocation) { + this.dispatchEvent('geolocation:unsupported'); + return; + } + + navigator.geolocation.getCurrentPosition( + (position) => {this.dispatchEvent('geolocation:position', {latitude: position.coords.latitude, longitude: position.coords.longitude});}, + (error) => {this.dispatchEvent('geolocation:error', {error: error});}, + params + ); + } + + watch({params}) { + if (!navigator.geolocation) { + this.dispatchEvent('geolocation:unsupported'); + return; + } + if (this.watchId) { + return; + } + + this.watchId = navigator.geolocation.watchPosition( + (position) => {this.dispatchEvent('geolocation:position', {latitude: position.coords.latitude, longitude: position.coords.longitude});}, + (error) => {this.dispatchEvent('geolocation:error', {error: error});}, + params + ); + } + + clearWatch() { + if (!this.watchId) { + return; + } + + navigator.geolocation.clearWatch(this.watchId); + this.watchId = null; + this.dispatchEvent('geolocation:watch:cleared'); + } +} diff --git a/assets/src/install_controller.js b/assets/src/install_controller.js new file mode 100644 index 0000000..87906b9 --- /dev/null +++ b/assets/src/install_controller.js @@ -0,0 +1,42 @@ +'use strict'; + +import AbstractController from './abstract_controller.js'; + +/* stimulusFetch: 'lazy' */ +export default class extends AbstractController { + static targets = ['install']; + installPrompt = null; + + async connect() { + this.disableInstallTargets(); + window.addEventListener("beforeinstallprompt", async (event) => { + event.preventDefault(); + this.installPrompt = event; + this.enableInstallTargets(); + }); + } + + async install() { + if (!this.installPrompt) { + return; + } + const result = await this.installPrompt.prompt(); + if (result.outcome === 'accepted') { + this.disableInstallTargets(); + } else { + this.dispatchEvent('install:cancelled'); + } + } + + enableInstallTargets() { + this.installTargets.forEach((installElement) => { + installElement.removeAttribute("hidden"); + }); + } + + disableInstallTargets() { + this.installTargets.forEach((installElement) => { + installElement.setAttribute("hidden", ""); + }); + } +} diff --git a/assets/src/prefetch-on-demand_controller.js b/assets/src/prefetch-on-demand_controller.js index f24d58e..e8e7d55 100644 --- a/assets/src/prefetch-on-demand_controller.js +++ b/assets/src/prefetch-on-demand_controller.js @@ -1,9 +1,9 @@ 'use strict'; -import { Controller } from '@hotwired/stimulus'; +import AbstractController from './abstract_controller.js'; /* stimulusFetch: 'lazy' */ -export default class extends Controller { +export default class extends AbstractController { prefetch = async ({params}) => { const workbox = window.workbox; if (!workbox || !params.urls) { @@ -21,8 +21,4 @@ export default class extends Controller { {params} ); } - - dispatchEvent = (name, payload) => { - this.dispatch(name, { detail: payload }); - } } diff --git a/assets/src/presentation_controller.js b/assets/src/presentation_controller.js new file mode 100644 index 0000000..cdccb54 --- /dev/null +++ b/assets/src/presentation_controller.js @@ -0,0 +1,69 @@ +'use strict'; + +import AbstractController from './abstract_controller.js'; + +/* stimulusFetch: 'lazy' */ +export default class extends AbstractController { + static values = { + urls: { type: Array }, + }; + + request = null; + connection = null; + async connect() { + this.request = new PresentationRequest(this.urlsValue); + const availability = await this.request.getAvailability(); + this.dispatchEvent('presentation:availability:changed', { availability }); + availability.onchange = () => { + this.dispatchEvent('presentation:availability:changed', { availability }); + } + } + + start = async () => { + if (!this.request) { + return; + } + const connection = await this.request.start(); + this.setConnection(connection); + } + + reconnect = async () => { + const connectionId = localStorage.getItem('presentation_connection_id'); + if (!connectionId) { + return; + } + + const connection = await this.request.reconnect(connectionId); + this.setConnection(connection); + } + + async send ({params}) { + if (!this.connection) { + return; + } + const {message} = params; + this.connection.send(message); + } + + terminate = () => { + if (!this.connection) { + return; + } + const id = this.connection.id; + this.connection.onclose = null; + this.connection.terminate(); + this.connection = null; + localStorage.removeItem('presentation_connection_id'); + this.dispatchEvent('presentation:terminated', {id}); + } + + setConnection(connection) { + if (this.connection) { + this.terminate(); + } + + this.connection = connection; + localStorage.setItem('presentation_connection_id', connection.id); + this.dispatchEvent('presentation:started', {id: connection.id}); + } +} diff --git a/assets/src/receiver_controller.js b/assets/src/receiver_controller.js new file mode 100644 index 0000000..934d5b6 --- /dev/null +++ b/assets/src/receiver_controller.js @@ -0,0 +1,27 @@ +'use strict'; + +import AbstractController from './abstract_controller.js'; + +/* stimulusFetch: 'lazy' */ +export default class extends AbstractController { + async connect() { + if (!navigator.presentation.receiver) { + } + const connections = await navigator.presentation.receiver.connections; + connections.connections.map(connection => addConnection(connection)); + connections.addEventListener( + 'connectionavailable', + (event) => { + const connection = event.connection; + connection.addEventListener( + 'message', + (event) => this.dispatchEvent('message', {message: event.data}) + ); + connection.addEventListener( + 'close', + () => this.dispatchEvent('close') + ); + } + ); + } +} diff --git a/assets/src/share_controller.js b/assets/src/share_controller.js new file mode 100644 index 0000000..80e0cdc --- /dev/null +++ b/assets/src/share_controller.js @@ -0,0 +1,24 @@ +'use strict'; + +import AbstractController from './abstract_controller.js'; + +/* stimulusFetch: 'lazy' */ +export default class extends AbstractController { + async share({params}) { + const {data} = params; + if (!data) { + console.error("No data provided"); + return; + } + try { + if (!navigator.canShare || !navigator.canShare(data)) { + console.error("Cannot share data"); + return; + } + await navigator.share(data); + this.dispatchEvent('share:success', {data}); + } catch (error) { + console.error("Error sharing", {error}); + } + } +} diff --git a/assets/src/sync-broadcast_controller.js b/assets/src/sync-broadcast_controller.js index fd7600c..19efde0 100644 --- a/assets/src/sync-broadcast_controller.js +++ b/assets/src/sync-broadcast_controller.js @@ -1,9 +1,9 @@ 'use strict'; -import { Controller } from '@hotwired/stimulus'; +import AbstractController from './abstract_controller.js'; /* stimulusFetch: 'lazy' */ -export default class extends Controller { +export default class extends AbstractController { static values = { channel: { type: String }, }; @@ -25,10 +25,6 @@ export default class extends Controller { } } - dispatchEvent = (name, payload) => { - this.dispatch(name, { detail: payload }); - } - messageReceived = async (event) => { const data = event.data; this.remainingTargets.forEach((element) => { diff --git a/assets/src/vibration_controller.js b/assets/src/vibration_controller.js new file mode 100644 index 0000000..034c748 --- /dev/null +++ b/assets/src/vibration_controller.js @@ -0,0 +1,45 @@ +'use strict'; + +import AbstractController from './abstract_controller.js'; + +/* stimulusFetch: 'lazy' */ +export default class extends AbstractController { + vibrateInterval = null; + + vibrate = async ({params}) => { + const { sequence } = params; + if (!sequence) { + console.error('Vibration sequence is required.'); + return; + } + await navigator.vibrate(sequence); + this.dispatchEvent('vibration:triggered', { sequence }); + } + + persistent = async ({params}) => { + if (this.vibrateInterval !== null) { + this.stop(); + } + const { sequence, duration } = params; + if (!sequence) { + console.error('Vibration sequence is required.'); + return; + } + if (!duration) { + console.error('Vibration duration is required.'); + return; + } + this.vibrateInterval = setInterval(() => { + startVibrate(sequence); + }, duration); + } + + stop = async () => { + if (this.vibrateInterval === null) { + return; + } + clearInterval(this.vibrateInterval); + this.vibrateInterval = null; + this.dispatchEvent('vibration:stopped'); + } +} diff --git a/castor.php b/castor.php index 5820b93..4beb2e6 100644 --- a/castor.php +++ b/castor.php @@ -1,9 +1,10 @@ 'coverage', - ]; - run($command, environment: $environment); + $context = context(); + $context = $context->withEnvironment(['XDEBUG_MODE' => 'coverage']); + run($command, context: $context); } #[AsTask(description: 'Run tests')] @@ -39,21 +39,20 @@ function test(bool $coverageHtml = false, bool $coverageText = false, null|strin { io()->title('Running tests'); $command = ['php', 'vendor/bin/phpunit', '--color']; - $environment = [ - 'XDEBUG_MODE' => 'off', - ]; + $context = context(); + $context = $context->withEnvironment(['XDEBUG_MODE' => 'off']); if ($coverageHtml) { $command[] = '--coverage-html=build/coverage'; - $environment['XDEBUG_MODE'] = 'coverage'; + $context = $context->withEnvironment(['XDEBUG_MODE' => 'coverage']); } if ($coverageText) { $command[] = '--coverage-text'; - $environment['XDEBUG_MODE'] = 'coverage'; + $context = $context->withEnvironment(['XDEBUG_MODE' => 'coverage']); } if ($group !== null) { $command[] = sprintf('--group=%s', $group); } - run($command, environment: $environment); + run($command, context: $context); } #[AsTask(description: 'Coding standards check')] @@ -65,16 +64,15 @@ function cs( ): void { io()->title('Running coding standards check'); $command = ['php', 'vendor/bin/ecs', 'check']; - $environment = [ - 'XDEBUG_MODE' => 'off', - ]; + $context = context(); + $context = $context->withEnvironment(['XDEBUG_MODE' => 'off']); if ($fix) { $command[] = '--fix'; } if ($clearCache) { $command[] = '--clear-cache'; } - run($command, environment: $environment); + run($command, context: $context); } #[AsTask(description: 'Running PHPStan')] @@ -88,10 +86,9 @@ function stan( if ($baseline) { $command[] = '--generate-baseline'; } - $environment = [ - 'XDEBUG_MODE' => 'off', - ]; - run($command, environment: $environment); + $context = context(); + $context = $context->withEnvironment(['XDEBUG_MODE' => 'off']); + run($command, context: $context); } #[AsTask(description: 'Validate Composer configuration')] @@ -99,13 +96,12 @@ function validate(): void { io()->title('Validating Composer configuration'); $command = ['composer', 'validate', '--strict']; - $environment = [ - 'XDEBUG_MODE' => 'off', - ]; - run($command, environment: $environment); + $context = context(); + $context = $context->withEnvironment(['XDEBUG_MODE' => 'off']); + run($command, context: $context); $command = ['composer', 'dump-autoload', '--optimize', '--strict-psr']; - run($command, environment: $environment); + run($command, context: $context); } /** @@ -118,10 +114,10 @@ function checkLicenses( io()->title('Checking licenses'); $allowedExceptions = []; $command = ['composer', 'licenses', '-f', 'json']; - $environment = [ - 'XDEBUG_MODE' => 'off', - ]; - $result = run($command, environment: $environment, quiet: true); + $context = context(); + $context = $context->withEnvironment(['XDEBUG_MODE' => 'off']); + $context = $context->withQuiet(); + $result = run($command, context: $context); if (! $result->isSuccessful()) { io()->error('Cannot determine licenses'); exit(1); @@ -181,10 +177,9 @@ function rector( if ($clearCache) { $command[] = '--clear-cache'; } - $environment = [ - 'XDEBUG_MODE' => 'off', - ]; - run($command, environment: $environment); + $context = context(); + $context = $context->withEnvironment(['XDEBUG_MODE' => 'off']); + run($command, context: $context); } #[AsTask(description: 'Run Rector')] @@ -192,10 +187,9 @@ function deptrac(): void { io()->title('Running Rector'); $command = ['php', 'vendor/bin/deptrac', 'analyse', '--fail-on-uncovered', '--no-cache']; - $environment = [ - 'XDEBUG_MODE' => 'off', - ]; - run($command, environment: $environment); + $context = context(); + $context = $context->withEnvironment(['XDEBUG_MODE' => 'off']); + run($command, context: $context); } #[AsTask(description: 'Run Linter')] @@ -203,8 +197,7 @@ function lint(): void { io()->title('Running Linter'); $command = ['composer', 'exec', '--', 'parallel-lint', __DIR__ . '/src/', __DIR__ . '/tests/']; - $environment = [ - 'XDEBUG_MODE' => 'off', - ]; - run($command, environment: $environment); + $context = context(); + $context = $context->withEnvironment(['XDEBUG_MODE' => 'off']); + run($command, context: $context); } diff --git a/link b/link old mode 100644 new mode 100755 diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e8ad28c..14f7bdc 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -455,11 +455,6 @@ parameters: count: 1 path: src/ImageProcessor/GDImageProcessor.php - - - message: "#^Should not use node with type \"Stmt_Echo\", please change the code\\.$#" - count: 3 - path: src/ImageProcessor/GDImageProcessor.php - - message: "#^PHPDoc tag @return with type array\\ is incompatible with native type string\\.$#" count: 1 diff --git a/phpstan.neon b/phpstan.neon index d550a2c..1dd34f5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,8 +7,10 @@ parameters: checkGenericClassInNonGenericObjectType: true checkUninitializedProperties: true treatPhpDocTypesAsCertain: false + banned_code: + non_ignorable: false scanFiles: - vendor/symfony/dependency-injection/Loader/Configurator/ContainerConfigurator.php includes: - vendor/phpstan/phpstan/conf/bleedingEdge.neon - - phpstan-baseline.neon + - phpstan-baseline.neon \ No newline at end of file diff --git a/src/CachingStrategy/AssetCache.php b/src/CachingStrategy/AssetCache.php index 7be0018..7e69c70 100644 --- a/src/CachingStrategy/AssetCache.php +++ b/src/CachingStrategy/AssetCache.php @@ -16,6 +16,7 @@ use Symfony\Component\Serializer\Encoder\JsonEncode; use Symfony\Component\Serializer\SerializerInterface; use function count; +use function sprintf; use const JSON_PRETTY_PRINT; use const JSON_THROW_ON_ERROR; use const JSON_UNESCAPED_SLASHES; diff --git a/src/CachingStrategy/ImageCache.php b/src/CachingStrategy/ImageCache.php index daaeffe..506add9 100644 --- a/src/CachingStrategy/ImageCache.php +++ b/src/CachingStrategy/ImageCache.php @@ -11,6 +11,7 @@ use SpomkyLabs\PwaBundle\Service\CanLogInterface; use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use function sprintf; final class ImageCache implements HasCacheStrategiesInterface, CanLogInterface { diff --git a/src/CachingStrategy/ManifestCache.php b/src/CachingStrategy/ManifestCache.php index 87dd1dc..712e9d6 100644 --- a/src/CachingStrategy/ManifestCache.php +++ b/src/CachingStrategy/ManifestCache.php @@ -10,6 +10,7 @@ use SpomkyLabs\PwaBundle\Dto\Workbox; use SpomkyLabs\PwaBundle\Service\CanLogInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use function sprintf; final class ManifestCache implements HasCacheStrategiesInterface, CanLogInterface { diff --git a/src/CachingStrategy/PreloadUrlsGeneratorManager.php b/src/CachingStrategy/PreloadUrlsGeneratorManager.php index c9ac49c..b740a19 100644 --- a/src/CachingStrategy/PreloadUrlsGeneratorManager.php +++ b/src/CachingStrategy/PreloadUrlsGeneratorManager.php @@ -10,6 +10,7 @@ use SpomkyLabs\PwaBundle\Service\CanLogInterface; use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; use function array_key_exists; +use function sprintf; final class PreloadUrlsGeneratorManager implements CanLogInterface { diff --git a/src/CachingStrategy/ResourceCaches.php b/src/CachingStrategy/ResourceCaches.php index 300d803..3fe2e3d 100644 --- a/src/CachingStrategy/ResourceCaches.php +++ b/src/CachingStrategy/ResourceCaches.php @@ -20,6 +20,7 @@ use Symfony\Component\Serializer\Encoder\JsonEncode; use Symfony\Component\Serializer\SerializerInterface; use function count; +use function sprintf; use const JSON_PRETTY_PRINT; use const JSON_THROW_ON_ERROR; use const JSON_UNESCAPED_SLASHES; diff --git a/src/CachingStrategy/WorkboxCacheStrategy.php b/src/CachingStrategy/WorkboxCacheStrategy.php index 73076ec..21086f6 100644 --- a/src/CachingStrategy/WorkboxCacheStrategy.php +++ b/src/CachingStrategy/WorkboxCacheStrategy.php @@ -6,6 +6,7 @@ use SpomkyLabs\PwaBundle\WorkboxPlugin\CachePluginInterface; use function in_array; +use function sprintf; use const JSON_PRETTY_PRINT; use const JSON_THROW_ON_ERROR; use const JSON_UNESCAPED_SLASHES; diff --git a/src/Command/CreateIconsCommand.php b/src/Command/CreateIconsCommand.php index 3d822cd..16d464b 100644 --- a/src/Command/CreateIconsCommand.php +++ b/src/Command/CreateIconsCommand.php @@ -19,6 +19,7 @@ use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Yaml\Yaml; use function is_string; +use function sprintf; #[AsCommand(name: 'pwa:create:icons', description: 'Generate icons for your PWA')] final class CreateIconsCommand extends Command diff --git a/src/Command/CreateScreenshotCommand.php b/src/Command/CreateScreenshotCommand.php index dcd6aa4..b4f02b3 100644 --- a/src/Command/CreateScreenshotCommand.php +++ b/src/Command/CreateScreenshotCommand.php @@ -24,6 +24,7 @@ use function assert; use function is_int; use function is_string; +use function sprintf; #[AsCommand( name: 'pwa:create:screenshot', @@ -155,7 +156,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($outputMimeType !== null) { $config['type'] = $outputMimeType; } - if ($title !== null && $title !== '') { + if ($title !== null) { $config['label'] = $title; } $io->success('Screenshot saved. You can now use it in your application configuration file.'); diff --git a/src/CompilerPass/PreloadUrlCompilerPass.php b/src/CompilerPass/PreloadUrlCompilerPass.php index 286e173..8ecbf9d 100644 --- a/src/CompilerPass/PreloadUrlCompilerPass.php +++ b/src/CompilerPass/PreloadUrlCompilerPass.php @@ -17,6 +17,7 @@ use Throwable; use function array_key_exists; use function is_string; +use function sprintf; /** * @internal diff --git a/src/ImageProcessor/Configuration.php b/src/ImageProcessor/Configuration.php index e64755f..fc4df4b 100644 --- a/src/ImageProcessor/Configuration.php +++ b/src/ImageProcessor/Configuration.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use Stringable; +use function sprintf; final readonly class Configuration implements Stringable { diff --git a/src/ImageProcessor/GDImageProcessor.php b/src/ImageProcessor/GDImageProcessor.php index 4153e0c..dc24801 100644 --- a/src/ImageProcessor/GDImageProcessor.php +++ b/src/ImageProcessor/GDImageProcessor.php @@ -46,7 +46,9 @@ public function process( $pngData = ob_get_clean(); assert(is_string($pngData)); + // @phpstan-ignore-next-line echo pack('v3', 0, 1, 1); + // @phpstan-ignore-next-line echo pack( 'C4v2V2', $configuration->width, @@ -58,6 +60,7 @@ public function process( mb_strlen($pngData, '8bit'), 22 ); + // @phpstan-ignore-next-line echo $pngData; break; default: @@ -121,11 +124,10 @@ private function createMainImage(string $image, Configuration $configuration): G } /*if ($configuration->width === $configuration->height) { - $mainImage = imagescale($mainImage, $configuration->width, $configuration->height); - assert($mainImage !== false); - - return $mainImage; - }*/ + * $mainImage = imagescale($mainImage, $configuration->width, $configuration->height); + * assert($mainImage !== false); + * return $mainImage; + * }*/ $srcWidth = imagesx($mainImage); $srcHeight = imagesy($mainImage); diff --git a/src/MatchCallbackHandler/DestinationMatchCallbackHandler.php b/src/MatchCallbackHandler/DestinationMatchCallbackHandler.php index ba4ff12..d99250c 100644 --- a/src/MatchCallbackHandler/DestinationMatchCallbackHandler.php +++ b/src/MatchCallbackHandler/DestinationMatchCallbackHandler.php @@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use SpomkyLabs\PwaBundle\Service\CanLogInterface; +use function sprintf; final class DestinationMatchCallbackHandler implements MatchCallbackHandlerInterface, CanLogInterface { diff --git a/src/MatchCallbackHandler/ExactPathnameMatchCallbackHandler.php b/src/MatchCallbackHandler/ExactPathnameMatchCallbackHandler.php index 050177a..a295b33 100644 --- a/src/MatchCallbackHandler/ExactPathnameMatchCallbackHandler.php +++ b/src/MatchCallbackHandler/ExactPathnameMatchCallbackHandler.php @@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use SpomkyLabs\PwaBundle\Service\CanLogInterface; +use function sprintf; final class ExactPathnameMatchCallbackHandler implements MatchCallbackHandlerInterface, CanLogInterface { diff --git a/src/MatchCallbackHandler/OriginMatchCallbackHandler.php b/src/MatchCallbackHandler/OriginMatchCallbackHandler.php index 7d0eb7a..f34f943 100644 --- a/src/MatchCallbackHandler/OriginMatchCallbackHandler.php +++ b/src/MatchCallbackHandler/OriginMatchCallbackHandler.php @@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use SpomkyLabs\PwaBundle\Service\CanLogInterface; +use function sprintf; final class OriginMatchCallbackHandler implements MatchCallbackHandlerInterface, CanLogInterface { diff --git a/src/MatchCallbackHandler/PathnameEndsWithMatchCallbackHandler.php b/src/MatchCallbackHandler/PathnameEndsWithMatchCallbackHandler.php index f134627..71ed16c 100644 --- a/src/MatchCallbackHandler/PathnameEndsWithMatchCallbackHandler.php +++ b/src/MatchCallbackHandler/PathnameEndsWithMatchCallbackHandler.php @@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use SpomkyLabs\PwaBundle\Service\CanLogInterface; +use function sprintf; final class PathnameEndsWithMatchCallbackHandler implements MatchCallbackHandlerInterface, CanLogInterface { diff --git a/src/MatchCallbackHandler/PathnameStartsWithMatchCallbackHandler.php b/src/MatchCallbackHandler/PathnameStartsWithMatchCallbackHandler.php index b643f86..d0ed63b 100644 --- a/src/MatchCallbackHandler/PathnameStartsWithMatchCallbackHandler.php +++ b/src/MatchCallbackHandler/PathnameStartsWithMatchCallbackHandler.php @@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use SpomkyLabs\PwaBundle\Service\CanLogInterface; +use function sprintf; final class PathnameStartsWithMatchCallbackHandler implements MatchCallbackHandlerInterface, CanLogInterface { diff --git a/src/MatchCallbackHandler/RouteMatchCallbackHandler.php b/src/MatchCallbackHandler/RouteMatchCallbackHandler.php index 2494790..bfc51cf 100644 --- a/src/MatchCallbackHandler/RouteMatchCallbackHandler.php +++ b/src/MatchCallbackHandler/RouteMatchCallbackHandler.php @@ -8,6 +8,7 @@ use Psr\Log\NullLogger; use SpomkyLabs\PwaBundle\Service\CanLogInterface; use Symfony\Component\Routing\RouterInterface; +use function sprintf; final class RouteMatchCallbackHandler implements MatchCallbackHandlerInterface, CanLogInterface { diff --git a/src/Normalizer/ScreenshotNormalizer.php b/src/Normalizer/ScreenshotNormalizer.php index 9efb9c1..f816a1e 100644 --- a/src/Normalizer/ScreenshotNormalizer.php +++ b/src/Normalizer/ScreenshotNormalizer.php @@ -13,6 +13,7 @@ use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use function assert; +use function sprintf; final class ScreenshotNormalizer implements NormalizerInterface, NormalizerAwareInterface { diff --git a/src/Service/FaviconsCompiler.php b/src/Service/FaviconsCompiler.php index 50e9fad..37fbb1c 100644 --- a/src/Service/FaviconsCompiler.php +++ b/src/Service/FaviconsCompiler.php @@ -15,6 +15,7 @@ use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; use function assert; +use function sprintf; use const PHP_EOL; final class FaviconsCompiler implements FileCompilerInterface, CanLogInterface diff --git a/src/Service/IconResolver.php b/src/Service/IconResolver.php index f13692b..07bf878 100644 --- a/src/Service/IconResolver.php +++ b/src/Service/IconResolver.php @@ -17,6 +17,7 @@ use function assert; use function count; use function is_array; +use function sprintf; final readonly class IconResolver { diff --git a/src/Service/ServiceWorkerCompiler.php b/src/Service/ServiceWorkerCompiler.php index be25c01..955fdd2 100644 --- a/src/Service/ServiceWorkerCompiler.php +++ b/src/Service/ServiceWorkerCompiler.php @@ -17,6 +17,7 @@ use function in_array; use function is_array; use function is_string; +use function sprintf; final class ServiceWorkerCompiler implements FileCompilerInterface, CanLogInterface { diff --git a/src/ServiceWorkerRule/AppendCacheStrategies.php b/src/ServiceWorkerRule/AppendCacheStrategies.php index cda6871..219a578 100644 --- a/src/ServiceWorkerRule/AppendCacheStrategies.php +++ b/src/ServiceWorkerRule/AppendCacheStrategies.php @@ -7,6 +7,7 @@ use SpomkyLabs\PwaBundle\CachingStrategy\HasCacheStrategiesInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; +use function sprintf; use const PHP_EOL; final readonly class AppendCacheStrategies implements ServiceWorkerRuleInterface diff --git a/src/Twig/PwaRuntime.php b/src/Twig/PwaRuntime.php index 32cd7e2..484a0aa 100644 --- a/src/Twig/PwaRuntime.php +++ b/src/Twig/PwaRuntime.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Mime\MimeTypes; use function array_key_exists; +use function sprintf; use const ENT_COMPAT; use const ENT_SUBSTITUTE; use const PHP_EOL; diff --git a/src/WorkboxPlugin/BroadcastUpdatePlugin.php b/src/WorkboxPlugin/BroadcastUpdatePlugin.php index 8d5011d..33fde5a 100644 --- a/src/WorkboxPlugin/BroadcastUpdatePlugin.php +++ b/src/WorkboxPlugin/BroadcastUpdatePlugin.php @@ -4,6 +4,8 @@ namespace SpomkyLabs\PwaBundle\WorkboxPlugin; +use function sprintf; + final readonly class BroadcastUpdatePlugin implements CachePluginInterface, HasDebugInterface { private const NAME = 'BroadcastUpdatePlugin'; diff --git a/src/WorkboxPlugin/CacheableResponsePlugin.php b/src/WorkboxPlugin/CacheableResponsePlugin.php index 011d145..6cec135 100644 --- a/src/WorkboxPlugin/CacheableResponsePlugin.php +++ b/src/WorkboxPlugin/CacheableResponsePlugin.php @@ -4,6 +4,8 @@ namespace SpomkyLabs\PwaBundle\WorkboxPlugin; +use function sprintf; + final readonly class CacheableResponsePlugin implements CachePluginInterface, HasDebugInterface { private const NAME = 'CacheableResponsePlugin'; diff --git a/src/WorkboxPlugin/ExpirationPlugin.php b/src/WorkboxPlugin/ExpirationPlugin.php index 79eb733..141b913 100644 --- a/src/WorkboxPlugin/ExpirationPlugin.php +++ b/src/WorkboxPlugin/ExpirationPlugin.php @@ -4,6 +4,8 @@ namespace SpomkyLabs\PwaBundle\WorkboxPlugin; +use function sprintf; + final readonly class ExpirationPlugin implements CachePluginInterface, HasDebugInterface { private const NAME = 'ExpirationPlugin'; diff --git a/tests/Functional/AbstractPwaTestCase.php b/tests/Functional/AbstractPwaTestCase.php index 64a27b5..046c286 100644 --- a/tests/Functional/AbstractPwaTestCase.php +++ b/tests/Functional/AbstractPwaTestCase.php @@ -8,6 +8,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Filesystem\Filesystem; use function assert; +use function sprintf; /** * @internal diff --git a/tests/Functional/GenerateIconsCommandTest.php b/tests/Functional/GenerateIconsCommandTest.php index 77b16c6..7e2b2ee 100644 --- a/tests/Functional/GenerateIconsCommandTest.php +++ b/tests/Functional/GenerateIconsCommandTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\Test; use Symfony\Component\Console\Tester\CommandTester; use function assert; +use function sprintf; /** * @internal diff --git a/tests/Functional/TakeScreenshotCommandTest.php b/tests/Functional/TakeScreenshotCommandTest.php index accd081..a3e2592 100644 --- a/tests/Functional/TakeScreenshotCommandTest.php +++ b/tests/Functional/TakeScreenshotCommandTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\Attributes\Test; use Symfony\Component\Console\Tester\CommandTester; use function assert; +use function sprintf; /** * @internal diff --git a/tests/TestFilesystem.php b/tests/TestFilesystem.php index 002a7b5..70a7e02 100644 --- a/tests/TestFilesystem.php +++ b/tests/TestFilesystem.php @@ -7,6 +7,7 @@ use Symfony\Component\AssetMapper\Path\PublicAssetsFilesystemInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use function dirname; +use function sprintf; final readonly class TestFilesystem implements PublicAssetsFilesystemInterface {