Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions module/Application/migrations/Version20250304130147.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Application\Migrations;

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

/**
* phpcs:disable Generic.Files.LineLength.TooLong
* phpcs:disable SlevomatCodingStandard.Functions.RequireMultiLineCall.RequiredMultiLineCall
*/
final class Version20250304130147 extends AbstractMigration
{
public function getDescription(): string
{
return 'Allow tagging of BM/GMM bodies in photos.';
}

public function up(Schema $schema): void
{
$this->addSql('DROP INDEX tag_idx ON Tag');
$this->addSql('ALTER TABLE Tag ADD body_id INT DEFAULT NULL, ADD type VARCHAR(255) NOT NULL, CHANGE member_id member_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE Tag ADD CONSTRAINT FK_3BC4F1639B621D84 FOREIGN KEY (body_id) REFERENCES Organ (id)');
$this->addSql('CREATE INDEX IDX_3BC4F1639B621D84 ON Tag (body_id)');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE Tag DROP FOREIGN KEY FK_3BC4F1639B621D84');
$this->addSql('DROP INDEX IDX_3BC4F1639B621D84 ON Tag');
$this->addSql('ALTER TABLE Tag DROP body_id, DROP type, CHANGE member_id member_id INT NOT NULL');
$this->addSql('CREATE UNIQUE INDEX tag_idx ON Tag (photo_id, member_id)');
}
}
21 changes: 12 additions & 9 deletions module/Decision/src/Model/Member.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\OneToOne;
use Photo\Model\Tag as TagModel;
use Photo\Model\MemberTag as MemberTagModel;
use Photo\Model\TaggableInterface;
use User\Model\User as UserModel;

/**
* Member model.
*
* @implements TaggableInterface<MemberTagModel>

Check failure on line 29 in module/Decision/src/Model/Member.php

View workflow job for this annotation

GitHub Actions / php-codesniffer / PHP_CodeSniffer (8.3)

Incorrect annotations group.
*
* @psalm-type MemberGdprArrayType = array{
* lidnr: int,
* email: ?string,
Expand All @@ -46,7 +49,7 @@
* }
*/
#[Entity]
class Member
class Member implements TaggableInterface
{
/**
* The user.
Expand Down Expand Up @@ -267,10 +270,10 @@
/**
* Member tags.
*
* @var Collection<array-key, TagModel>
* @var Collection<array-key, MemberTagModel>
*/
#[OneToMany(
targetEntity: TagModel::class,
targetEntity: MemberTagModel::class,
mappedBy: 'member',
fetch: 'EXTRA_LAZY',
)]
Expand Down Expand Up @@ -306,6 +309,11 @@
return $this->lidnr;
}

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

/**
* Get the member's email address.
*/
Expand Down Expand Up @@ -645,12 +653,7 @@
return $this->boardInstallations;
}

/**
* Get the tags.
*
* @return Collection<array-key, TagModel>
*/
public function getTags(): Collection

Check failure on line 656 in module/Decision/src/Model/Member.php

View workflow job for this annotation

GitHub Actions / php-codesniffer / PHP_CodeSniffer (8.3)

Method \Decision\Model\Member::getTags() does not have @return annotation for its traversable return value.
{
return $this->tags;
}
Expand Down
24 changes: 23 additions & 1 deletion module/Decision/src/Model/Organ.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\OneToOne;
use Photo\Model\BodyTag as BodyTagModel;
use Photo\Model\TaggableInterface;

use function usort;

/**
* Organ entity.
*
* Note that this entity is derived from the decisions themself.
*
* @implements TaggableInterface<BodyTagModel>
*/
#[Entity]
class Organ
class Organ implements TaggableInterface
{
use IdentifiableTrait;

Expand Down Expand Up @@ -158,11 +162,24 @@
)]
protected Collection $organInformation;

/**
* Body tags.
*
* @var Collection<array-key, BodyTagModel>
*/
#[OneToMany(
targetEntity: BodyTagModel::class,
mappedBy: 'body',
fetch: 'EXTRA_LAZY',
)]
protected Collection $tags;

public function __construct()
{
$this->members = new ArrayCollection();
$this->subdecisions = new ArrayCollection();
$this->organInformation = new ArrayCollection();
$this->tags = new ArrayCollection();
}

/**
Expand Down Expand Up @@ -358,4 +375,9 @@
return null !== $this->abrogationDate
&& (new DateTime()) >= $this->abrogationDate;
}

public function getTags(): Collection

Check failure on line 379 in module/Decision/src/Model/Organ.php

View workflow job for this annotation

GitHub Actions / php-codesniffer / PHP_CodeSniffer (8.3)

Method \Decision\Model\Organ::getTags() does not have @return annotation for its traversable return value.
{
return $this->tags;
}
}
5 changes: 3 additions & 2 deletions module/Photo/config/module.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@
'tag' => [
'type' => Segment::class,
'options' => [
'route' => '/tag/:lidnr',
'route' => '/tag/:type/:id',
'constraints' => [
'lidnr' => '[0-9]+',
'type' => '(body|member)',
'id' => '[0-9]+',
],
'defaults' => [
'controller' => TagController::class,
Expand Down
10 changes: 6 additions & 4 deletions module/Photo/src/Controller/TagController.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ public function addAction(): JsonModel

if ($request->isPost()) {
$photoId = (int) $this->params()->fromRoute('photo_id');
$lidnr = (int) $this->params()->fromRoute('lidnr');
$tag = $this->photoService->addTag($photoId, $lidnr);
$type = $this->params()->fromRoute('type');
$id = (int) $this->params()->fromRoute('id');
$tag = $this->photoService->addTag($photoId, $type, $id);

if (null === $tag) {
$result['success'] = false;
Expand All @@ -59,8 +60,9 @@ public function removeAction(): JsonModel

if ($request->isPost()) {
$photoId = (int) $this->params()->fromRoute('photo_id');
$lidnr = (int) $this->params()->fromRoute('lidnr');
$result['success'] = $this->photoService->removeTag($photoId, $lidnr);
$type = $this->params()->fromRoute('type');
$id = (int) $this->params()->fromRoute('id');
$result['success'] = $this->photoService->removeTag($photoId, $type, $id);
}

return new JsonModel($result);
Expand Down
57 changes: 44 additions & 13 deletions module/Photo/src/Mapper/Tag.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

use Application\Mapper\BaseMapper;
use Decision\Model\Member as MemberModel;
use Doctrine\ORM\EntityRepository;

Check failure on line 9 in module/Photo/src/Mapper/Tag.php

View workflow job for this annotation

GitHub Actions / php-codesniffer / PHP_CodeSniffer (8.3)

Type Doctrine\ORM\EntityRepository is not used in this file.
use Doctrine\ORM\Query\ResultSetMapping;
use Photo\Model\BodyTag as BodyTagModel;
use Photo\Model\MemberTag as MemberTagModel;
use Photo\Model\Photo as PhotoModel;
use Photo\Model\Tag as TagModel;

Expand All @@ -17,14 +20,24 @@
*/
class Tag extends BaseMapper
{
/**
* @psalm-param 'body'|'member' $type
*/
public function findTag(
int $photoId,
int $lidnr,
string $type,
int $id,
): ?TagModel {
return $this->getRepository()->findOneBy(
if ('body' === $type) {
$repository = $this->getEntityManager()->getRepository(BodyTagModel::class);
} else {
$repository = $this->getEntityManager()->getRepository(MemberTagModel::class);
}

return $repository->findOneBy(
[
'photo' => $photoId,
'member' => $lidnr,
$type => $id,
],
);
}
Expand All @@ -34,7 +47,7 @@
*/
public function getTagsByLidnr(int $lidnr): array
{
return $this->getRepository()->findBy(

Check failure on line 50 in module/Photo/src/Mapper/Tag.php

View workflow job for this annotation

GitHub Actions / PHPStan

Call to method Doctrine\ORM\EntityRepository<Photo\Model\Tag>::findBy() - entity Photo\Model\Tag does not have a field named $member.
[
'member' => $lidnr,
],
Expand All @@ -42,29 +55,45 @@
}

/**
* Get all the tags for a photo, but limited to lidnr and full name.
* Get all the tags for a photo, including lidnr and full name for members, or id, abbr, and type for bodies.
*
* @return array<array-key, array{
* id: int,
* lidnr: int,
* fullName: string,
* tagged_id: int,
* tagged_name: string,
* tagged_type: string,
* body_type: ?string,
* }>
*/
public function getTagsByPhoto(int $photoId): array
{
$rsm = new ResultSetMapping();
$rsm->addScalarResult('id', 'id', 'integer')
->addScalarResult('lidnr', 'lidnr', 'integer')
->addScalarResult('fullName', 'fullName');
->addScalarResult('tagged_id', 'tagged_id', 'integer')
->addScalarResult('tagged_name', 'tagged_name')
->addScalarResult('tagged_type', 'tagged_type')
->addScalarResult('body_type', 'body_type');

// phpcs:disable Generic.Files.LineLength.TooLong -- no need to split this query more
$sql = <<<'QUERY'
SELECT
`t`.`id`,
`m`.`lidnr`,
CONCAT_WS(' ', `m`.`firstName`, IF(LENGTH(`m`.`middleName`), `m`.`middleName`, NULL), `m`.`lastName`) as `fullName`
FROM `Member` `m`
LEFT JOIN `Tag` `t` ON `m`.`lidnr` = `t`.`member_id`
CASE
WHEN `t`.`type` = 'member' THEN `m`.`lidnr`
WHEN `t`.`type` = 'body' THEN `b`.`id`
END AS `tagged_id`,
CASE
WHEN `t`.`type` = 'member' THEN CONCAT_WS(' ', `m`.`firstName`, IF(LENGTH(`m`.`middleName`), `m`.`middleName`, NULL), `m`.`lastName`)
WHEN `t`.`type` = 'body' THEN `b`.`abbr`
END AS `tagged_name`,
`t`.`type` AS `tagged_type`,
CASE
WHEN `t`.`type` = 'body' THEN `b`.`type`
ELSE NULL
END AS `body_type`
FROM `Tag` `t`
LEFT JOIN `Member` `m` ON `t`.`member_id` = `m`.`lidnr` AND `t`.`type` = 'member'
LEFT JOIN `Organ` `b` ON `t`.`body_id` = `b`.`id` AND `t`.`type` = 'body'
WHERE `t`.`photo_id` = :photo_id
QUERY;
// phpcs:enable Generic.Files.LineLength.TooLong
Expand All @@ -73,8 +102,9 @@
$query->setParameter(':photo_id', $photoId);

return $query->getArrayResult();
}

Check failure on line 105 in module/Photo/src/Mapper/Tag.php

View workflow job for this annotation

GitHub Actions / php-codesniffer / PHP_CodeSniffer (8.3)

Expected 1 blank line after function; 2 found


/**
* Get all unique albums a certain member is tagged in
*
Expand All @@ -91,6 +121,7 @@
FROM `Photo` `p`
LEFT JOIN `Tag` `t` ON `t`.`photo_id` = `p`.`id`
WHERE `t`.`member_id` = :member_id
AND `t`.`body_id` IS NULL
QUERY;

$query = $this->getEntityManager()->createNativeQuery($sql, $rsm);
Expand All @@ -110,7 +141,7 @@

// Retrieve the lidnr of the member with the most tags
$qb->select('IDENTITY(t.member), COUNT(t.member) as tag_count')
->from($this->getRepositoryName(), 't')
->from(MemberTagModel::class, 't')
->where('t.member IN (?1)')
->setParameter(1, $members)
->groupBy('t.member')
Expand All @@ -133,7 +164,7 @@
->setMaxResults(1)
->orderBy('p.dateTime', 'DESC');

$res = $qb2->getQuery()->getResult();

Check failure on line 167 in module/Photo/src/Mapper/Tag.php

View workflow job for this annotation

GitHub Actions / PHPStan

QueryBuilder: [Semantical Error] line 0, col 91 near 'member = ?1 ORDER': Error: Class Photo\Model\Tag has no field or association named member

if (empty($res)) {
return null;
Expand Down
57 changes: 57 additions & 0 deletions module/Photo/src/Model/BodyTag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Photo\Model;

use Decision\Model\Organ as OrganModel;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\UniqueConstraint;
use InvalidArgumentException;

use function get_class;

/**
* A tag in a photo for an organ (BM/GMM body).
*
* @extends Tag<OrganModel>
*/
#[Entity]
#[UniqueConstraint(fields: ['photo', 'body'])]
class BodyTag extends Tag
{
#[ManyToOne(
targetEntity: OrganModel::class,
inversedBy: 'tags',
)]
#[JoinColumn(
name: 'body_id',
referencedColumnName: 'id',
nullable: true,
)]
protected OrganModel $body;

public function getTagged(): OrganModel
{
return $this->body;
}

/**
* @psalm-param OrganModel $tagged
*/
public function setTagged(TaggableInterface $tagged): void
{
if (!($tagged instanceof OrganModel)) {
throw new InvalidArgumentException(sprintf('Expected Organ got %s...', get_class($tagged)));

Check failure on line 47 in module/Photo/src/Model/BodyTag.php

View workflow job for this annotation

GitHub Actions / php-codesniffer / PHP_CodeSniffer (8.3)

Function sprintf() should not be referenced via a fallback global name, but via a use statement.

Check failure on line 47 in module/Photo/src/Model/BodyTag.php

View workflow job for this annotation

GitHub Actions / php-codesniffer / PHP_CodeSniffer (8.3)

Class name referenced via call of function get_class().
}

$this->body = $tagged;
}

public function getType(): string
{
return 'organ';
}
}
Loading
Loading