Skip to content

Feature: Qr-code hit count #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: feature/4495-seperating-qr-design
Choose a base branch
from
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ See [keep a changelog] for information about writing changes to this log.

## [Unreleased]

[PR-25](https://github.com/itk-dev/itqr/pull/25)
- Qr hit counter
[PR-23](https://github.com/itk-dev/itqr/pull/23)
- Seperate visual representation of QR into own config
- Live preview of design and download
Expand Down
33 changes: 33 additions & 0 deletions migrations/Version20250613101601.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250613101601 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE qr_hit_tracker (id INT AUTO_INCREMENT NOT NULL, qr_id INT DEFAULT NULL, timestamp DATETIME NOT NULL, INDEX IDX_E53FF9E35AA64A57 (qr_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE qr_hit_tracker ADD CONSTRAINT FK_E53FF9E35AA64A57 FOREIGN KEY (qr_id) REFERENCES qr (id)');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE qr_hit_tracker DROP FOREIGN KEY FK_E53FF9E35AA64A57');
$this->addSql('DROP TABLE qr_hit_tracker');
}
}
12 changes: 12 additions & 0 deletions src/Controller/Admin/QrCrudController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\Controller\Admin\Embed\UrlCrudController;
use App\Entity\Tenant\Qr;
use App\Helper\DownloadHelper;
use App\Repository\QrHitTrackerRepository;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Assets;
Expand All @@ -17,6 +18,7 @@
use EasyCorp\Bundle\EasyAdminBundle\Field\CollectionField;
use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Filter\ChoiceFilter;
Expand All @@ -32,6 +34,7 @@ class QrCrudController extends AbstractTenantAwareCrudController
{
public function __construct(
private readonly DownloadHelper $downloadHelper,
private readonly QrHitTrackerRepository $hitTrackerRepository,
) {
}

Expand Down Expand Up @@ -67,6 +70,15 @@ public function configureFields(string $pageName): iterable
Field::new('customUrlButton', new TranslatableMessage('qr.preview'))
->setTemplatePath('fields/link/link.html.twig')
->hideOnForm(),
IntegerField::new('hitTrackers', new TranslatableMessage('Hits'))
->formatValue(function ($value, $entity) {
if (null === $entity) {
return '0';
}

return $this->hitTrackerRepository->getHitCount($entity);
})
->hideOnForm(),
];
}

Expand Down
11 changes: 10 additions & 1 deletion src/Controller/QrController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace App\Controller;

use App\Entity\QrHitTracker;
use App\Repository\QrRepository;
use App\Repository\UrlRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -18,7 +20,7 @@ public function __construct(
}

#[Route('/qr/{uuid}', name: 'app_qr_index')]
public function index(string $uuid, UrlRepository $urlRepository): Response
public function index(string $uuid, UrlRepository $urlRepository, EntityManagerInterface $entityManager): Response
{
// Find the QR entity by UUID
$uuid = UuidV7::fromString($uuid);
Expand All @@ -28,6 +30,13 @@ public function index(string $uuid, UrlRepository $urlRepository): Response
throw $this->createNotFoundException('QR code not found');
}

// Create QR hit tracker entry
$qrHitTracker = new QrHitTracker();
$qrHitTracker->setQr($qr);
$qrHitTracker->setTimestamp(new \DateTimeImmutable());
$entityManager->persist($qrHitTracker);
$entityManager->flush();

$urls = $qr->getUrls();

// @TODO: Add what happens if a qr has multiple urls with certain modes.
Expand Down
53 changes: 53 additions & 0 deletions src/Entity/QrHitTracker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace App\Entity;

use App\Entity\Tenant\Qr;
use App\Repository\QrHitTrackerRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: QrHitTrackerRepository::class)]
class QrHitTracker
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;

#[ORM\ManyToOne(targetEntity: Qr::class, inversedBy: 'hitTrackers')]
#[ORM\JoinColumn(nullable: true)]
private ?Qr $qr = null;

#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: false)]
private \DateTimeInterface $timestamp;

public function getId(): ?int
{
return $this->id;
}

public function getQr(): Qr
{
return $this->qr;
}

public function setQr(Qr $qr): static
{
$this->qr = $qr;

return $this;
}

public function getTimestamp(): ?\DateTimeInterface
{
return $this->timestamp;
}

public function setTimestamp(\DateTimeInterface $timestamp): static
{
$this->timestamp = $timestamp;

return $this;
}
}
13 changes: 13 additions & 0 deletions src/Entity/Tenant/Qr.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use App\Entity\QrHitTracker;
use App\Enum\QrModeEnum;
use App\Repository\QrRepository;
use Doctrine\Common\Collections\ArrayCollection;
Expand Down Expand Up @@ -45,11 +46,18 @@ class Qr extends AbstractTenantScopedEntity
#[Assert\Valid]
private Collection $urls;

/**
* @var Collection<int, QrHitTracker>
*/
#[ORM\OneToMany(targetEntity: QrHitTracker::class, mappedBy: 'qr')]
private Collection $hitTrackers;

public function __construct()
{
parent::__construct();

$this->urls = new ArrayCollection();
$this->hitTrackers = new ArrayCollection();
$this->uuid = Uuid::v7();
}

Expand Down Expand Up @@ -143,4 +151,9 @@ public function removeAllUrls(): void
$this->removeUrl($url);
}
}

public function getHitTrackers(): Collection
{
return $this->hitTrackers;
}
}
24 changes: 24 additions & 0 deletions src/Repository/QrHitTrackerRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App\Repository;

use App\Entity\QrHitTracker;
use App\Entity\Tenant\Qr;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
* @extends ServiceEntityRepository<QrHitTracker>
*/
class QrHitTrackerRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, QrHitTracker::class);
}

public function getHitCount(Qr $qr): int
{
return $this->count(['qr' => $qr]);
}
}
4 changes: 3 additions & 1 deletion templates/fields/link/link.html.twig
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<a href="{{ path('qr_code_generate', {builder: 'default', data: entity.instance.uuid}) }}" target="_blank" class="popover-link">
{% set qr_path = path('qr_code_generate', {builder: 'default', data: url('app_qr_index', {uuid: entity.instance.uuid})}) %}

<a href="{{ qr_path }}" target="_blank" class="popover-link">
{{ 'qr.view'|trans }}
<span class="popover-content">
<img src="{{ path('qr_code_generate', {builder: 'default', data: url('app_qr_index', {uuid: entity.instance.uuid})}) }}" alt="Generated Image" />
Expand Down