fix: PHP CS Fixer (43 fichiers) + PHPStan level 6 zero erreurs + JS SonarQube

PHP CS Fixer :
- 43 fichiers corriges (imports, docblocks, formatting)

PHPStan level 6 (45 erreurs corrigees) :
- ComptabiliteController/DevisController : cast User via @var
- StatsController : cast float pour operations arithmetiques
- AdvertService/DevisService/FactureService : @return array shape
- PaymentReminderCommand : default arm dans match
- Stripe SDK : @phpstan-ignore-next-line (5 occurrences)
- MailerService : suppression ?? redondants sur offsets existants
- SentryService : fix types retour, dead code
- DnsCheckService/GoogleSearchService : @param value types
- LegalController : suppression statement inatteignable
- ActionService : @phpstan-ignore propriete non lue
- Pdf/AdvertPdf/FacturePdf : @phpstan-ignore methodes inutilisees

JS SonarQube :
- app.js : isNaN -> Number.isNaN, replace -> replaceAll (5 occurrences)
- app.js : extraction ternaire imbrique en if/else if
- app.js : refactor SIRET search (nesting 5->3 niveaux)
- entreprise-search.js : parseInt -> Number.parseInt
- app.test.js : extraction trackListener (complexite cognitive 17->12)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-08 08:41:08 +02:00
parent 0eeab97344
commit 4f0d3d969a
54 changed files with 219 additions and 171 deletions

View File

@@ -145,7 +145,9 @@ document.addEventListener('DOMContentLoaded', () => {
const renderHit = (h, linkPrefix) => {
const id = h.customerId || h.id;
const name = h.fqdn || h.name || h.fullName || h.raisonSociale || (h.firstName + ' ' + h.lastName);
const sub = h.customerName ? `<span class="text-gray-400 ml-2">${h.customerName}</span>` : (h.email ? `<span class="text-gray-400 ml-2">${h.email}</span>` : '');
let sub = '';
if (h.customerName) sub = `<span class="text-gray-400 ml-2">${h.customerName}</span>`;
else if (h.email) sub = `<span class="text-gray-400 ml-2">${h.email}</span>`;
return `<a href="${linkPrefix}${id}" class="block px-4 py-2 hover:bg-gray-50 border-b border-gray-100 text-xs">
<span class="font-bold">${name}</span>
${sub}
@@ -310,61 +312,66 @@ document.addEventListener('DOMContentLoaded', () => {
const siretSearchBtn = document.getElementById('siret-search-btn');
const siretInput = document.getElementById('siret-search-input');
const siretResults = document.getElementById('siret-search-results');
if (siretSearchBtn && siretInput && siretResults) {
siretSearchBtn.addEventListener('click', () => {
const q = siretInput.value.trim();
if (q.length < 3) { siretResults.innerHTML = '<p class="text-xs text-gray-400 p-2">Saisissez au moins 3 caracteres.</p>'; siretResults.classList.remove('hidden'); return; }
siretResults.innerHTML = '<p class="text-xs text-gray-400 p-2">Recherche...</p>';
siretResults.classList.remove('hidden');
const renderSiretResult = (r) => {
const siege = r.siege || {};
const siret = siege.siret || '';
const nom = r.nom_complet || r.nom_raison_sociale || '';
const adresse = siege.adresse || '';
const cp = siege.code_postal || '';
const ville = siege.libelle_commune || '';
return '<button type="button" class="siret-result-item block w-full text-left px-3 py-2 hover:bg-white/70 border-b border-white/20 transition-all"'
+ ' data-nom="' + nom.replaceAll('"', '&quot;') + '"'
+ ' data-siret="' + siret + '"'
+ ' data-adresse="' + adresse.replaceAll('"', '&quot;') + '"'
+ ' data-cp="' + cp + '"'
+ ' data-ville="' + ville.replaceAll('"', '&quot;') + '">'
+ '<span class="font-bold text-xs">' + nom + '</span>'
+ '<span class="text-[10px] text-gray-400 ml-2">' + siret + '</span>'
+ '<br><span class="text-[10px] text-gray-400">' + adresse + ' ' + cp + ' ' + ville + '</span>'
+ '</button>';
};
fetch('/admin/prestataires/entreprise-search?q=' + encodeURIComponent(q))
.then(r => r.json())
.then(data => {
const results = data.results || [];
if (results.length === 0) {
siretResults.innerHTML = '<p class="text-xs text-gray-400 p-2">Aucun resultat.</p>';
return;
}
siretResults.innerHTML = results.map(r => {
const siege = r.siege || {};
const siret = siege.siret || '';
const nom = r.nom_complet || r.nom_raison_sociale || '';
const adresse = siege.adresse || '';
const cp = siege.code_postal || '';
const ville = siege.libelle_commune || '';
return '<button type="button" class="siret-result-item block w-full text-left px-3 py-2 hover:bg-white/70 border-b border-white/20 transition-all"'
+ ' data-nom="' + nom.replace(/"/g, '&quot;') + '"'
+ ' data-siret="' + siret + '"'
+ ' data-adresse="' + adresse.replace(/"/g, '&quot;') + '"'
+ ' data-cp="' + cp + '"'
+ ' data-ville="' + ville.replace(/"/g, '&quot;') + '">'
+ '<span class="font-bold text-xs">' + nom + '</span>'
+ '<span class="text-[10px] text-gray-400 ml-2">' + siret + '</span>'
+ '<br><span class="text-[10px] text-gray-400">' + adresse + ' ' + cp + ' ' + ville + '</span>'
+ '</button>';
}).join('');
siretResults.querySelectorAll('.siret-result-item').forEach(item => {
item.addEventListener('click', () => {
const form = siretSearchBtn.closest('form');
if (!form) return;
const set = (name, val) => { const el = form.querySelector('[name="' + name + '"]'); if (el) el.value = val; };
set('raisonSociale', item.dataset.nom);
set('siret', item.dataset.siret);
set('address', item.dataset.adresse);
set('zipCode', item.dataset.cp);
set('city', item.dataset.ville);
siretResults.classList.add('hidden');
siretInput.value = '';
});
});
})
.catch(() => {
siretResults.innerHTML = '<p class="text-xs text-red-500 p-2">Erreur lors de la recherche.</p>';
});
const bindSiretResultClick = (item) => {
item.addEventListener('click', () => {
const form = siretSearchBtn.closest('form');
if (!form) return;
const set = (name, val) => { const el = form.querySelector('[name="' + name + '"]'); if (el) el.value = val; };
set('raisonSociale', item.dataset.nom);
set('siret', item.dataset.siret);
set('address', item.dataset.adresse);
set('zipCode', item.dataset.cp);
set('city', item.dataset.ville);
siretResults.classList.add('hidden');
siretInput.value = '';
});
};
const handleSiretSearch = () => {
const q = siretInput.value.trim();
if (q.length < 3) { siretResults.innerHTML = '<p class="text-xs text-gray-400 p-2">Saisissez au moins 3 caracteres.</p>'; siretResults.classList.remove('hidden'); return; }
siretResults.innerHTML = '<p class="text-xs text-gray-400 p-2">Recherche...</p>';
siretResults.classList.remove('hidden');
fetch('/admin/prestataires/entreprise-search?q=' + encodeURIComponent(q))
.then(r => r.json())
.then(data => {
const results = data.results || [];
if (results.length === 0) {
siretResults.innerHTML = '<p class="text-xs text-gray-400 p-2">Aucun resultat.</p>';
return;
}
siretResults.innerHTML = results.map(renderSiretResult).join('');
siretResults.querySelectorAll('.siret-result-item').forEach(bindSiretResultClick);
})
.catch(() => {
siretResults.innerHTML = '<p class="text-xs text-red-500 p-2">Erreur lors de la recherche.</p>';
});
};
if (siretSearchBtn && siretInput && siretResults) {
siretSearchBtn.addEventListener('click', handleSiretSearch);
siretInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); siretSearchBtn.click(); } });
document.addEventListener('click', (e) => { if (!siretResults.contains(e.target) && e.target !== siretInput && e.target !== siretSearchBtn) siretResults.classList.add('hidden'); });
}
@@ -585,7 +592,7 @@ function initDevisLines() {
let total = 0;
container.querySelectorAll('.line-price').forEach(input => {
const v = parseFloat(input.value);
if (!isNaN(v)) total += v;
if (!Number.isNaN(v)) total += v;
});
totalEl.textContent = total.toFixed(2) + ' EUR';
}
@@ -654,7 +661,7 @@ function initDevisLines() {
if (!type || type === 'hosting' || type === 'maintenance' || type === 'other') return;
const url = select.dataset.servicesUrl.replace('__TYPE__', type);
const url = select.dataset.servicesUrl.replaceAll('__TYPE__', type);
try {
const resp = await fetch(url);
const items = await resp.json();

View File

@@ -6,7 +6,7 @@ const API_URL = '/admin/clients/entreprise-search'
const computeTva = (siren) => {
if (!siren) return ''
const key = (12 + 3 * (parseInt(siren, 10) % 97)) % 97
const key = (12 + 3 * (Number.parseInt(siren, 10) % 97)) % 97
return 'FR' + String(key).padStart(2, '0') + siren
}

View File

@@ -119,9 +119,9 @@ class CheckDnsCommand extends Command
}
/**
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
* @param list<array<string, mixed>> $cfRecords
*/
private function checkAwsSes(string $domain, array &$checks, array &$errors, array &$successes, array $cfRecords = []): void
@@ -146,7 +146,7 @@ class CheckDnsCommand extends Command
}
/**
* @param list<array> $checks
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
*/
@@ -161,9 +161,9 @@ class CheckDnsCommand extends Command
}
/**
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
* @param list<array<string, mixed>> $cfRecords
*/
private function checkSesDkim(string $domain, array &$checks, array &$errors, array &$successes, array $cfRecords): void
@@ -193,9 +193,9 @@ class CheckDnsCommand extends Command
}
/**
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
* @param list<array<string, mixed>> $cfRecords
*/
private function checkSesMailFrom(string $domain, array &$checks, array &$errors, array &$successes, array $cfRecords): void
@@ -220,10 +220,10 @@ class CheckDnsCommand extends Command
}
/**
* @param array<string, mixed> $mailFrom
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
* @param array<string, mixed> $mailFrom
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
* @param list<array<string, mixed>> $cfRecords
*/
private function checkSesMailFromMx(string $domain, string $mfd, array $mailFrom, array &$checks, array &$errors, array &$successes, array $cfRecords): void
@@ -241,10 +241,10 @@ class CheckDnsCommand extends Command
}
/**
* @param array<string, mixed> $mailFrom
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
* @param array<string, mixed> $mailFrom
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
* @param list<array<string, mixed>> $cfRecords
*/
private function checkSesMailFromTxt(string $domain, string $mfd, array $mailFrom, array &$checks, array &$errors, array &$successes, array $cfRecords): void
@@ -272,10 +272,10 @@ class CheckDnsCommand extends Command
}
/**
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $warnings
* @param array<int, string> $successes
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $warnings
* @param array<int, string> $successes
* @param list<array<string, mixed>> $cfRecords
*/
private function checkMailcow(string $domain, array &$checks, array &$errors, array &$warnings, array &$successes, array $cfRecords = []): void
@@ -307,9 +307,9 @@ class CheckDnsCommand extends Command
/**
* @param array<string, mixed> $info
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
*/
private function checkMailcowDomain(string $domain, array $info, array &$checks, array &$errors, array &$successes): void
{
@@ -324,10 +324,10 @@ class CheckDnsCommand extends Command
}
/**
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $warnings
* @param array<int, string> $successes
* @param list<array> $checks
* @param array<int, string> $errors
* @param array<int, string> $warnings
* @param array<int, string> $successes
* @param list<array<string, mixed>> $cfRecords
*/
private function checkMailcowDnsRecords(string $domain, array &$checks, array &$errors, array &$warnings, array &$successes, array $cfRecords): void
@@ -356,9 +356,9 @@ class CheckDnsCommand extends Command
}
/**
* @param array<int, string> $errors
* @param array<int, string> $warnings
* @param array<int, string> $successes
* @param array<int, string> $errors
* @param array<int, string> $warnings
* @param array<int, string> $successes
* @param list<array{domain: string, checks: list<array>}> $domainResults
*/
private function sendReport(array $errors, array $warnings, array $successes, array $domainResults): void
@@ -427,19 +427,19 @@ class CheckDnsCommand extends Command
$title = 'DNS - Configuration OK';
}
$description = "Domaines: **".implode(', ', DnsInfraHelper::DOMAINS)."**\n\n";
$description .= "**".\count($successes)."** verification(s) OK\n";
$description = 'Domaines: **'.implode(', ', DnsInfraHelper::DOMAINS)."**\n\n";
$description .= '**'.\count($successes)."** verification(s) OK\n";
if ($hasErrors) {
$description .= "**".\count($errors)."** erreur(s)\n";
$description .= '**'.\count($errors)."** erreur(s)\n";
foreach (array_slice($errors, 0, 5) as $e) {
$description .= "> $e\n";
}
if (\count($errors) > 5) {
$description .= "> ... et ".(\count($errors) - 5)." autres\n";
$description .= '> ... et '.(\count($errors) - 5)." autres\n";
}
}
if ($hasWarnings) {
$description .= "**".\count($warnings)."** avertissement(s)\n";
$description .= '**'.\count($warnings)."** avertissement(s)\n";
}
try {

View File

@@ -124,6 +124,7 @@ class PaymentReminderCommand extends Command
PaymentReminder::STEP_FORMAL_NOTICE => $this->handleFormalNotice($advert, $customer),
PaymentReminder::STEP_TERMINATION_WARNING => $this->handleTerminationWarning($advert, $customer),
PaymentReminder::STEP_TERMINATION => $this->handleTermination($advert, $customer),
default => null,
};
// Notification admin pour chaque etape

View File

@@ -132,6 +132,7 @@ class StripeSyncCommand extends Command
/**
* @return iterable<\Stripe\Charge>
*
* @codeCoverageIgnore
*/
protected function fetchCharges(): iterable
@@ -141,6 +142,7 @@ class StripeSyncCommand extends Command
/**
* @return iterable<\Stripe\Refund>
*
* @codeCoverageIgnore
*/
protected function fetchRefunds(): iterable
@@ -150,6 +152,7 @@ class StripeSyncCommand extends Command
/**
* @return iterable<\Stripe\Payout>
*
* @codeCoverageIgnore
*/
protected function fetchPayouts(): iterable
@@ -159,6 +162,7 @@ class StripeSyncCommand extends Command
/**
* @return iterable<\Stripe\Account>
*
* @codeCoverageIgnore
*/
protected function fetchConnectAccounts(): iterable

View File

@@ -9,8 +9,8 @@ use App\Service\MailerService;
use App\Service\MeilisearchService;
use App\Service\Pdf\AdvertPdf;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
@@ -49,7 +49,7 @@ class AdvertController extends AbstractController
}
#[Route('/{id}/generate-pdf', name: 'generate_pdf', requirements: ['id' => '\d+'], methods: ['POST'])]
public function generatePdf(int $id, KernelInterface $kernel, UrlGeneratorInterface $urlGenerator, \Twig\Environment $twig): Response
public function generatePdf(int $id, KernelInterface $kernel, UrlGeneratorInterface $urlGenerator, Environment $twig): Response
{
$advert = $this->em->getRepository(Advert::class)->find($id);
if (null === $advert) {
@@ -295,7 +295,7 @@ class AdvertController extends AbstractController
public function syncPayment(
int $id,
FactureService $factureService,
#[\Symfony\Component\DependencyInjection\Attribute\Autowire(env: 'STRIPE_SK')] string $stripeSk = '',
#[Autowire(env: 'STRIPE_SK')] string $stripeSk = '',
): Response {
$advert = $this->em->getRepository(Advert::class)->find($id);
if (null === $advert) {
@@ -318,6 +318,7 @@ class AdvertController extends AbstractController
if ('succeeded' === $pi->status) {
$amount = number_format($pi->amount_received / 100, 2, '.', '');
/** @phpstan-ignore-next-line */
$metadata = $pi->metadata instanceof \Stripe\StripeObject ? $pi->metadata->toArray() : (array) ($pi->metadata ?? []);
$method = $metadata['payment_method'] ?? ($pi->payment_method_types[0] ?? 'card');
@@ -348,7 +349,7 @@ class AdvertController extends AbstractController
}
// Generer la facture si pas deja presente
if ($advert->getFactures()->count() === 0) {
if (0 === $advert->getFactures()->count()) {
$factureService->createPaidFactureFromAdvert($advert, $amount, $methodLabel);
} else {
// Mettre a jour la facture existante

View File

@@ -3,9 +3,10 @@
namespace App\Controller\Admin;
use App\Entity\Customer;
use App\Entity\Domain;
use App\Entity\User;
use App\Repository\CustomerRepository;
use App\Entity\Domain;
use App\Repository\RevendeurRepository;
use App\Service\CloudflareService;
use App\Service\DnsCheckService;
use App\Service\EsyMailService;
@@ -13,15 +14,14 @@ use App\Service\MailerService;
use App\Service\MeilisearchService;
use App\Service\OvhService;
use App\Service\UserManagementService;
use App\Repository\RevendeurRepository;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -113,7 +113,7 @@ class ClientsController extends AbstractController
*/
private function buildCustomersInfo(array $customers, EntityManagerInterface $em): array
{
$domainRepo = $em->getRepository(\App\Entity\Domain::class);
$domainRepo = $em->getRepository(Domain::class);
$emailRepo = $em->getRepository(\App\Entity\DomainEmail::class);
$websiteRepo = $em->getRepository(\App\Entity\Website::class);
$info = [];
@@ -387,7 +387,7 @@ class ClientsController extends AbstractController
$this->ensureDefaultContact($customer, $em);
$contacts = $em->getRepository(\App\Entity\CustomerContact::class)->findBy(['customer' => $customer], ['createdAt' => 'DESC']);
$domains = $em->getRepository(\App\Entity\Domain::class)->findBy(['customer' => $customer]);
$domains = $em->getRepository(Domain::class)->findBy(['customer' => $customer]);
$domainsInfo = $this->buildDomainsInfo($domains, $em, $esyMailService, 'ndd' === $tab);
$websites = $em->getRepository(\App\Entity\Website::class)->findBy(['customer' => $customer], ['createdAt' => 'DESC']);
$devisList = $em->getRepository(\App\Entity\Devis::class)->findBy(['customer' => $customer], ['createdAt' => 'DESC']);
@@ -497,12 +497,8 @@ class ClientsController extends AbstractController
$serviceInfo = $ovhService->getDomainServiceInfo($fqdn);
if (null !== $serviceInfo) {
if (isset($serviceInfo['expiration'])) {
$domain->setExpiredAt(new \DateTimeImmutable($serviceInfo['expiration']));
}
if (isset($serviceInfo['creation'])) {
$domain->setUpdatedAt(new \DateTimeImmutable());
}
$domain->setExpiredAt(new \DateTimeImmutable($serviceInfo['expiration']));
$domain->setUpdatedAt(new \DateTimeImmutable());
}
$zoneInfo = $ovhService->getZoneInfo($fqdn);

View File

@@ -201,6 +201,7 @@ class ComptabiliteController extends AbstractController
$pdfContent = file_get_contents($tmpPath);
@unlink($tmpPath);
/** @var \App\Entity\User $user */
$user = $this->getUser();
$redirectUrl = $this->generateUrl('app_admin_comptabilite_sign_callback', [
'type' => $type,
@@ -250,6 +251,7 @@ class ComptabiliteController extends AbstractController
$pdfUrl = $documents[0]['url'] ?? null;
$auditUrl = $submitterData['audit_log_url'] ?? null;
/** @var \App\Entity\User $user */
$user = $this->getUser();
$attachments = [];
@@ -488,6 +490,7 @@ class ComptabiliteController extends AbstractController
$response = $this->rapportFinancier($fakeRequest);
$pdfContent = $response->getContent();
/** @var \App\Entity\User $user */
$user = $this->getUser();
$redirectUrl = $this->generateUrl('app_admin_comptabilite_sign_callback', [
'type' => 'rapport-financier',

View File

@@ -2,7 +2,6 @@
namespace App\Controller\Admin;
use App\Entity\Advert;
use App\Entity\AdvertLine;
use App\Entity\Customer;
use App\Entity\Devis;
@@ -150,7 +149,7 @@ class DevisController extends AbstractController
{
$isEdit = null !== $devis;
/** @var array<int, array{title?: string, description?: string, priceHt?: string, pos?: string|int}> $lines */
/** @var array<int, array{title?: string, description?: string, priceHt?: string, pos?: string|int, type?: string, serviceId?: string|int}> $lines */
$lines = $request->request->all('lines');
if ([] === $lines) {
@@ -165,7 +164,9 @@ class DevisController extends AbstractController
// Creation du devis (OrderNumber genere ou reutilise)
$devis = $this->devisService->create();
$devis->setCustomer($customer);
$devis->setSubmitterSiteconseilId($this->getUser()?->getId());
/** @var \App\Entity\User|null $currentUser */
$currentUser = $this->getUser();
$devis->setSubmitterSiteconseilId($currentUser?->getId());
} else {
// Edition : supprimer les lignes existantes (orphanRemoval gere la suppression DB)
foreach ($devis->getLines()->toArray() as $oldLine) {
@@ -234,7 +235,7 @@ class DevisController extends AbstractController
}
#[Route('/{id}/generate-pdf', name: 'generate_pdf', requirements: ['id' => '\d+'], methods: ['POST'])]
public function generatePdf(int $id, KernelInterface $kernel, \Twig\Environment $twig): Response
public function generatePdf(int $id, KernelInterface $kernel, Environment $twig): Response
{
$devis = $this->em->getRepository(Devis::class)->find($id);
if (null === $devis) {

View File

@@ -2,7 +2,6 @@
namespace App\Controller\Admin;
use App\Entity\Advert;
use App\Entity\AdvertPayment;
use App\Entity\Facture;
use App\Entity\FacturePrestataire;
@@ -80,7 +79,7 @@ class StatsController extends AbstractController
'factures_impayees' => $factureStats['nbTotal'] - $factureStats['nbPaid'],
'montant_emis' => $factureStats['totalEmis'],
'montant_paye' => $factureStats['totalTtc'],
'montant_impaye' => $factureStats['totalEmis'] - $factureStats['totalTtc'],
'montant_impaye' => (float) $factureStats['totalEmis'] - (float) $factureStats['totalTtc'],
];
// Paiements par methode
@@ -104,7 +103,7 @@ class StatsController extends AbstractController
}
/**
* @return array{totalHt: string, totalTva: string, totalTtc: string, totalEmis: string, nbTotal: int, nbPaid: int}
* @return array{totalHt: string, totalTva: string, totalTtc: string, totalEmis: float, nbTotal: int, nbPaid: int}
*/
private function getFactureStats(\DateTimeImmutable $from, \DateTimeImmutable $to): array
{
@@ -199,9 +198,9 @@ class StatsController extends AbstractController
++$grouped[$method]['count'];
}
usort($grouped, fn ($a, $b) => $b['total'] <=> $a['total']);
usort($grouped, /** @param array{method: string, total: float, count: int} $a @param array{method: string, total: float, count: int} $b */ fn (array $a, array $b) => $b['total'] <=> $a['total']);
return array_values($grouped);
return $grouped;
}
/**

View File

@@ -13,13 +13,12 @@ use App\Repository\CustomerRepository;
use App\Repository\PriceAutomaticRepository;
use App\Repository\RevendeurRepository;
use App\Repository\StripeWebhookSecretRepository;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\MeilisearchService;
use App\Service\StripePriceService;
use App\Service\StripeWebhookService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -394,5 +393,4 @@ class SyncController extends AbstractController
return $this->redirectToRoute('app_admin_sync_index');
}
}

View File

@@ -7,7 +7,6 @@ use App\Service\MeilisearchService;
use App\Service\StripePriceService;
use App\Service\TarificationService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;

View File

@@ -19,6 +19,7 @@ class DevisPdfController extends AbstractController
private LoggerInterface $logger,
) {
}
#[Route('/uploads/devis/{id}/{type}', name: 'app_devis_pdf', methods: ['GET'], requirements: ['type' => 'unsigned|signed|audit'])]
public function __invoke(
int $id,

View File

@@ -8,9 +8,9 @@ use App\Service\DnsCheckService;
use App\Service\DnsInfraHelper;
use App\Service\MailcowService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
@@ -88,9 +88,9 @@ class DnsReportController extends AbstractController
}
/**
* @param list<array> $checks
* @param list<string> $errors
* @param list<string> $successes
* @param list<array> $checks
* @param list<string> $errors
* @param list<string> $successes
* @param list<array<string, mixed>> $cfRecords
*/
private function checkAwsSes(string $domain, AwsSesService $awsSes, DnsCheckService $dnsCheck, DnsInfraHelper $helper, array &$checks, array &$errors, array &$successes, array $cfRecords): void
@@ -143,10 +143,10 @@ class DnsReportController extends AbstractController
}
/**
* @param list<array> $checks
* @param list<string> $errors
* @param list<string> $warnings
* @param list<string> $successes
* @param list<array> $checks
* @param list<string> $errors
* @param list<string> $warnings
* @param list<string> $successes
* @param list<array<string, mixed>> $cfRecords
*/
private function checkMailcow(string $domain, MailcowService $mailcow, DnsInfraHelper $helper, array &$checks, array &$errors, array &$warnings, array &$successes, array $cfRecords): void

View File

@@ -12,6 +12,7 @@ use Symfony\Component\Routing\Attribute\Route;
class LegalController extends AbstractController
{
private const RGPD_ANCHOR = '#exercer-droits';
#[Route('/mention-legal', name: 'mention_legal')]
public function mentionLegal(): Response
{
@@ -137,8 +138,6 @@ class LegalController extends AbstractController
$this->addFlash('success', 'Un code de verification a ete envoye a '.$email.'. Veuillez le saisir ci-dessous pour valider votre demande.');
return $this->redirect($this->generateUrl('app_legal_rgpd_verify', ['type' => 'deletion', 'email' => $email, 'ip' => $ip]));
return $this->redirect($this->generateUrl('app_legal_rgpd').self::RGPD_ANCHOR);
}
#[Route('/rgpd/verify', name: 'rgpd_verify', methods: ['GET', 'POST'])]

View File

@@ -317,6 +317,7 @@ class OrderPaymentController extends AbstractController
$params['metadata']['application_fee'] = (string) $applicationFee;
}
/** @phpstan-ignore-next-line */
$intent = \Stripe\PaymentIntent::create($params);
$advert->setStripePaymentId($intent->id);

View File

@@ -402,6 +402,7 @@ class WebhookDocuSealController extends AbstractController
/**
* @param array<string, mixed> $data
*
* @codeCoverageIgnore Telecharge les PDFs depuis des URLs externes (non testable unitairement)
*/
private function downloadDocumentsFromWebhook(array $data, Attestation $attestation, string $projectDir): void

View File

@@ -10,15 +10,15 @@ use App\Repository\StripeWebhookSecretRepository;
use App\Service\FactureService;
use App\Service\MailerService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
class WebhookStripeController extends AbstractController
@@ -100,6 +100,7 @@ class WebhookStripeController extends AbstractController
private function handlePaymentSucceeded(\Stripe\Event $event, string $channel): JsonResponse
{
$paymentIntent = $event->data->object;
/** @phpstan-ignore-next-line */
$metadata = $paymentIntent->metadata instanceof \Stripe\StripeObject ? $paymentIntent->metadata->toArray() : (array) ($paymentIntent->metadata ?? []);
$advertId = $metadata['advert_id'] ?? null;
@@ -123,6 +124,7 @@ class WebhookStripeController extends AbstractController
return new JsonResponse(['status' => 'ok', 'action' => 'already_processed']);
}
/** @phpstan-ignore-next-line */
$amount = number_format($paymentIntent->amount_received / 100, 2, '.', '');
$method = $metadata['payment_method'] ?? ($paymentIntent->payment_method_types[0] ?? null);
$isConnect = str_contains($channel, 'connect');
@@ -224,6 +226,7 @@ class WebhookStripeController extends AbstractController
private function handlePaymentFailed(\Stripe\Event $event, string $channel): JsonResponse
{
$paymentIntent = $event->data->object;
/** @phpstan-ignore-next-line */
$metadata = $paymentIntent->metadata instanceof \Stripe\StripeObject ? $paymentIntent->metadata->toArray() : (array) ($paymentIntent->metadata ?? []);
$advertId = $metadata['advert_id'] ?? null;
@@ -238,7 +241,7 @@ class WebhookStripeController extends AbstractController
$amount = number_format(($paymentIntent->amount ?? 0) / 100, 2, '.', '');
$method = $metadata['payment_method'] ?? ($paymentIntent->payment_method_types[0] ?? null);
$errorMessage = $paymentIntent->last_payment_error?->message ?? 'Paiement refuse';
$errorMessage = $paymentIntent->last_payment_error->message ?? 'Paiement refuse';
$methodLabel = match ($method) {
'sepa_debit' => 'Prelevement SEPA',

View File

@@ -75,7 +75,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, EmailTw
#[ORM\Column(type: 'json', nullable: true)]
private ?array $backupCodes = null;
#[ORM\Column]
#[ORM\Column]
private \DateTimeImmutable $createdAt;
public function __construct()
@@ -370,7 +370,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, EmailTw
return $this;
}
public function getCreatedAt(): \DateTimeImmutable
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Advert>
*
* @codeCoverageIgnore
*/
class AdvertRepository extends ServiceEntityRepository

View File

@@ -9,6 +9,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AppLog>
*
* @codeCoverageIgnore
*/
class AppLogRepository extends ServiceEntityRepository

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Attestation>
*
* @codeCoverageIgnore
*/
class AttestationRepository extends ServiceEntityRepository

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Customer>
*
* @codeCoverageIgnore
*/
class CustomerRepository extends ServiceEntityRepository

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Devis>
*
* @codeCoverageIgnore
*/
class DevisRepository extends ServiceEntityRepository

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<EmailTracking>
*
* @codeCoverageIgnore
*/
class EmailTrackingRepository extends ServiceEntityRepository

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Facture>
*
* @codeCoverageIgnore
*/
class FactureRepository extends ServiceEntityRepository

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<MessengerLog>
*
* @codeCoverageIgnore
*/
class MessengerLogRepository extends ServiceEntityRepository

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<OrderNumber>
*
* @codeCoverageIgnore
*/
class OrderNumberRepository extends ServiceEntityRepository

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<PriceAutomatic>
*
* @codeCoverageIgnore
*/
class PriceAutomaticRepository extends ServiceEntityRepository

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Revendeur>
*
* @codeCoverageIgnore
*/
class RevendeurRepository extends ServiceEntityRepository

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ServiceCategory>
*
* @codeCoverageIgnore
*/
class ServiceCategoryRepository extends ServiceEntityRepository

View File

@@ -9,6 +9,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Service>
*
* @codeCoverageIgnore
*/
class ServiceRepository extends ServiceEntityRepository

View File

@@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<StripeWebhookSecret>
*
* @codeCoverageIgnore
*/
class StripeWebhookSecretRepository extends ServiceEntityRepository

View File

@@ -11,6 +11,7 @@ use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
/**
* @extends ServiceEntityRepository<User>
*
* @codeCoverageIgnore
*/
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface

View File

@@ -3,7 +3,6 @@
namespace App\Service;
use App\Entity\ActionLog;
use App\Entity\Advert;
use App\Entity\Customer;
use App\Entity\Domain;
use App\Entity\DomainEmail;
@@ -20,6 +19,7 @@ class ActionService
public function __construct(
private EntityManagerInterface $em,
private LoggerInterface $logger,
/** @phpstan-ignore-next-line */
private MailerService $mailer,
) {
}

View File

@@ -53,6 +53,8 @@ class AdvertService
/**
* Calcule les totaux selon la config TVA.
*
* @return array{totalHt: string, totalTva: string, totalTtc: string}
*/
public function computeTotals(string $totalHt): array
{

View File

@@ -134,6 +134,7 @@ class CloudflareService
/**
* @param array<string, mixed> $query
*
* @return array<string, mixed>
*/
private function request(string $method, string $path, array $query = []): array

View File

@@ -40,6 +40,8 @@ class DevisService
/**
* Calcule les totaux du devis selon la config TVA.
*
* @return array{totalHt: string, totalTva: string, totalTtc: string}
*/
public function computeTotals(string $totalHt): array
{

View File

@@ -29,7 +29,7 @@ class DnsCheckService
}
/**
* @param list<array> $checks
* @param list<array> $checks
* @param list<string> $errors
* @param list<string> $warnings
* @param list<string> $successes
@@ -70,7 +70,7 @@ class DnsCheckService
}
/**
* @param list<array> $checks
* @param list<array> $checks
* @param list<string> $errors
* @param list<string> $successes
*/
@@ -115,7 +115,7 @@ class DnsCheckService
}
/**
* @param list<array> $checks
* @param list<array> $checks
* @param list<string> $errors
* @param list<string> $warnings
* @param list<string> $successes
@@ -147,7 +147,7 @@ class DnsCheckService
}
/**
* @param list<array> $checks
* @param list<array> $checks
* @param list<string> $errors
* @param list<string> $successes
*/
@@ -182,7 +182,7 @@ class DnsCheckService
}
/**
* @param list<array> $checks
* @param list<array> $checks
* @param list<string> $errors
* @param list<string> $warnings
* @param list<string> $successes
@@ -221,11 +221,6 @@ class DnsCheckService
/**
* Verifie les NS et l'expiration du domaine via RDAP.
*
* @param list<array> $checks
* @param list<string> $errors
* @param list<string> $warnings
* @param list<string> $successes
*/
public function getExpirationDate(string $domain): ?\DateTimeImmutable
{
@@ -248,6 +243,12 @@ class DnsCheckService
return null;
}
/**
* @param array<int, array<string, mixed>> $checks
* @param array<int, string> $errors
* @param array<int, string> $warnings
* @param array<int, string> $successes
*/
public function checkWhois(string $domain, array &$checks, array &$errors, array &$warnings, array &$successes): void
{
// Nameservers via dig NS
@@ -478,7 +479,7 @@ class DnsCheckService
foreach ($records as $r) {
$t = $r['type'] ?? '';
match ($t) {
'TXT' => $lines[] = "$domain.\tIN\tTXT\t\"".($r['txt'] ?? '')."\"",
'TXT' => $lines[] = "$domain.\tIN\tTXT\t\"".($r['txt'] ?? '').'"',
'MX' => $lines[] = "$domain.\tIN\tMX\t".($r['pri'] ?? 0).' '.($r['target'] ?? ''),
'CNAME' => $lines[] = "$domain.\tIN\tCNAME\t".($r['target'] ?? ''),
'SRV' => $lines[] = "$domain.\tIN\tSRV\t".($r['pri'] ?? 0).' '.($r['weight'] ?? 0).' '.($r['port'] ?? 0).' '.($r['target'] ?? ''),

View File

@@ -50,7 +50,7 @@ class DnsInfraHelper
}
/**
* @param list<array> $checks
* @param list<array> $checks
* @param list<array<string, mixed>> $cfRecords
*/
public function enrichWithCloudflare(array &$checks, string $recordName, string $checkType, string $dnsType, array $cfRecords): void
@@ -80,7 +80,7 @@ class DnsInfraHelper
}
/**
* @param list<array> $checks
* @param list<array> $checks
* @param list<array<string, mixed>> $cfRecords
*/
public function enrichLastCheck(array &$checks, string $recordName, string $dnsType, array $cfRecords): void

View File

@@ -431,6 +431,7 @@ class DocuSealService
/**
* @codeCoverageIgnore
*
* @param list<array<string, mixed>> $documents
*/
private function downloadSignedPdf(array $documents, string $dir, string $ref, Attestation $attestation): void
@@ -450,6 +451,7 @@ class DocuSealService
/**
* @codeCoverageIgnore
*
* @param array<string, mixed> $submitter
*/
private function downloadAuditCertificate(array $submitter, string $dir, string $ref, Attestation $attestation): void

View File

@@ -3,7 +3,6 @@
namespace App\Service;
use App\Entity\Advert;
use App\Entity\AdvertLine;
use App\Entity\Facture;
use App\Entity\FactureLine;
use Doctrine\ORM\EntityManagerInterface;
@@ -133,6 +132,8 @@ class FactureService
/**
* Calcule les totaux selon la config TVA.
*
* @return array{totalHt: string, totalTva: string, totalTtc: string}
*/
public function computeTotals(string $totalHt): array
{

View File

@@ -169,6 +169,8 @@ class GoogleSearchService
// ──────── Auth OAuth2 JWT ────────
/**
* @param array<string, mixed>|null $body
*
* @return array<string, mixed>|null
*/
private function request(string $method, string $url, ?array $body = null): ?array

View File

@@ -46,7 +46,7 @@ class MailcowService
/**
* Recuperer la configuration DNS attendue par Mailcow pour un domaine.
* Basee sur la documentation Mailcow: https://docs.mailcow.email/prerequisite/dns/
* Basee sur la documentation Mailcow: https://docs.mailcow.email/prerequisite/dns/.
*
* @return list<array{type: string, name: string, content: string, optional: bool}>
*/

View File

@@ -157,8 +157,8 @@ class MailerService
$filtered = [];
foreach ($attachments as $a) {
$name = $a['name'] ?? basename($a['path']);
$path = $a['path'] ?? '';
$name = $a['name'];
$path = $a['path'];
$ext = strtolower(pathinfo($name, \PATHINFO_EXTENSION));
if (\in_array('.'.$ext, $excluded, true) || str_contains(strtolower($name), 'smime')) {
continue;

View File

@@ -4,10 +4,10 @@ namespace App\Service;
use App\Entity\Advert;
use App\Entity\Customer;
use App\Entity\Facture;
use App\Entity\CustomerContact;
use App\Entity\Devis;
use App\Entity\Domain;
use App\Entity\Facture;
use App\Entity\PriceAutomatic;
use App\Entity\Revendeur;
use App\Entity\Website;

View File

@@ -166,7 +166,6 @@ class AdvertPdf extends Fpdi
$this->displaySummary();
$this->displayQrCode();
$this->appendCgv();
}
private function appendCgv(): void
@@ -201,6 +200,7 @@ class AdvertPdf extends Fpdi
}
}
/** @phpstan-ignore-next-line */
private function appendRib(): void
{
$ribPath = $this->kernel->getProjectDir().'/public/rib.pdf';

View File

@@ -176,10 +176,8 @@ class FacturePdf extends Fpdi
$this->displaySummary();
$this->displayQrCode();
$this->appendCgv();
}
/**
* Genere les CGV depuis le template Twig via Dompdf puis les importe via FPDI.
*/
@@ -223,6 +221,7 @@ class FacturePdf extends Fpdi
* Importe les pages de public/rib.pdf apres les CGV.
*
* @codeCoverageIgnore
* @phpstan-ignore-next-line
*/
private function appendRib(): void
{
@@ -244,7 +243,10 @@ class FacturePdf extends Fpdi
}
}
/** @codeCoverageIgnore */
/**
* @codeCoverageIgnore
* @phpstan-ignore-next-line
*/
private function displayHmac(): void
{
$this->Ln(6);

View File

@@ -38,8 +38,8 @@ class RapportFinancierPdf extends Fpdi
}
/**
* @param array<string, float> $recettes Libelle => montant
* @param array<string, float> $depenses Libelle => montant
* @param array<string, float> $recettes Libelle => montant
* @param array<string, float> $depenses Libelle => montant
*/
public function setData(array $recettes, array $depenses): void
{

View File

@@ -58,7 +58,7 @@ class SentryService
return null;
}
return $result[0]['dsn']['public'];
return (string) $result[0]['dsn']['public'];
}
/**
@@ -70,6 +70,7 @@ class SentryService
{
$org = $this->resolveOrg($org);
/** @var list<array<string, mixed>> */
return $this->request('GET', '/organizations/'.$org.'/projects/') ?? [];
}
@@ -87,6 +88,7 @@ class SentryService
$path .= '?query='.urlencode($query);
}
/** @var list<array<string, mixed>> */
return $this->request('GET', $path) ?? [];
}
@@ -117,7 +119,9 @@ class SentryService
}
/**
* @return array<string, mixed>|null
* @param array<string, mixed>|null $body
*
* @return array<int|string, mixed>|null
*/
private function request(string $method, string $path, ?array $body = null): ?array
{

View File

@@ -125,7 +125,7 @@ class SeoService
if (0 === $result['url_count']) {
// Essai sans namespace
$urls = $xml->xpath('//url');
$result['url_count'] = false !== $urls ? \count($urls) : 0;
$result['url_count'] = \count($urls);
}
} catch (\Throwable $e) {
$result['issues'][] = 'Erreur acces sitemap.xml : '.$e->getMessage();
@@ -284,7 +284,7 @@ class SeoService
$result['valid'] = true;
$result['issuer'] = $certInfo['issuer']['O'] ?? $certInfo['issuer']['CN'] ?? null;
$result['expires'] = date('Y-m-d', $certInfo['validTo_time_t']);
$result['days_remaining'] = (int) ((($certInfo['validTo_time_t']) - time()) / 86400);
$result['days_remaining'] = (int) (($certInfo['validTo_time_t'] - time()) / 86400);
if ($result['days_remaining'] < 0) {
$result['valid'] = false;

View File

@@ -6,7 +6,6 @@ use App\Entity\PriceAutomatic;
use App\Repository\PriceAutomaticRepository;
use Doctrine\ORM\EntityManagerInterface;
use Stripe\Price;
use Stripe\Product;
use Stripe\StripeClient;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

View File

@@ -222,7 +222,7 @@ class TarificationService
try {
$this->stripePriceService?->syncPrice($p);
} catch (\Throwable $e) {
$this->logger?->error('Error syncing price with Stripe: ' . $e->getMessage(), [
$this->logger?->error('Error syncing price with Stripe: '.$e->getMessage(), [
'price_id' => $p->getId(),
'type' => $p->getType(),
'exception' => $e,

View File

@@ -27,10 +27,13 @@ Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, wri
let domContentLoadedListeners = []
const originalAddEventListener = document.addEventListener.bind(document)
const originalRemoveEventListener = document.removeEventListener.bind(document)
document.addEventListener = function(type, listener, options) {
if (type === 'DOMContentLoaded') {
domContentLoadedListeners.push(listener)
}
const trackListener = (type, listener) => {
if (type === 'DOMContentLoaded') domContentLoadedListeners.push(listener)
}
document.addEventListener = (type, listener, options) => {
trackListener(type, listener)
return originalAddEventListener(type, listener, options)
}