Skip to content

Commit a489ac6

Browse files
committed
API implementation for MultiSelect & MultiSelectOption
1 parent a0f941e commit a489ac6

20 files changed

+306
-85
lines changed

api/config/services.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ services:
4848
App\DataPersister\ContentNode\StoryboardDataPersister:
4949
arguments: [ '@api_platform.doctrine.orm.data_persister' ]
5050

51+
App\DataPersister\ContentNode\MultiSelectDataPersister:
52+
arguments: [ '@api_platform.doctrine.orm.data_persister' ]
53+
5154
App\DataPersister\Util\DataPersisterObservable:
5255
arguments: [ '@api_platform.doctrine.orm.data_persister' ]
5356

api/fixtures/multiSelects.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ App\Entity\ContentNode\MultiSelectOption:
2626
multiSelect: '@multiSelect1'
2727
translateKey: <word()>
2828
checked: true
29+
pos: 0
2930
multiSelectOption2:
3031
multiSelect: '@multiSelect2'
3132
translateKey: <word()>
3233
checked: true
34+
pos: 0
3335
multiSelectOptionCampUnrelated:
3436
multiSelect: '@multiSelectCampUnrelated'
3537
translateKey: <word()>
3638
checked: true
39+
pos: 0
3740

api/fixtures/storyboards.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@ App\Entity\ContentNode\StoryboardSection:
2727
column1: <word()>
2828
column2: <word()>
2929
column3: <word()>
30+
pos: 0
3031
storyboardSection2:
3132
storyboard: '@storyboard2'
3233
column1: <word()>
3334
column2: <word()>
3435
column3: <word()>
36+
pos: 0
3537
storyboardSectionCampUnrelated:
3638
storyboard: '@storyboardCampUnrelated'
3739
column1: <word()>
3840
column2: <word()>
3941
column3: <word()>
42+
pos: 0
4043

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace App\DataPersister\ContentNode;
4+
5+
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
6+
use App\Entity\ContentNode\MultiSelect;
7+
use App\Entity\ContentNode\MultiSelectOption;
8+
9+
class MultiSelectDataPersister extends ContentNodeBaseDataPersister implements ContextAwareDataPersisterInterface {
10+
public function supports($multiSelect, array $context = []): bool {
11+
return ($multiSelect instanceof MultiSelect) && $this->dataPersister->supports($multiSelect, $context);
12+
}
13+
14+
/**
15+
* @param MultiSelect $multiSelect
16+
*/
17+
public function onCreate($multiSelect) {
18+
if (isset($multiSelect->prototype)) {
19+
// copy from Prototype
20+
21+
if (!($multiSelect->prototype instanceof MultiSelect)) {
22+
throw new \Exception('Prototype must be of type MultiSelect');
23+
}
24+
25+
/** @var MultiSelect $prototype */
26+
$prototype = $multiSelect->prototype;
27+
28+
// copy all multiSelect options
29+
foreach ($prototype->options as $prototypeOption) {
30+
$option = new MultiSelectOption();
31+
32+
$option->translateKey = $prototypeOption->translateKey;
33+
$option->checked = $prototypeOption->checked;
34+
$option->setPos($prototypeOption->getPos());
35+
36+
$multiSelect->addOption($option);
37+
}
38+
} else {
39+
// no prototype given --> copy from ContentType config
40+
41+
foreach ($multiSelect->contentType->jsonConfig['items'] as $key => $item) {
42+
$option = new MultiSelectOption();
43+
44+
$option->translateKey = $item;
45+
$option->setPos($key);
46+
47+
$multiSelect->addOption($option);
48+
}
49+
}
50+
51+
parent::onCreate($multiSelect);
52+
}
53+
}

api/src/DataPersister/ContentNode/StoryboardDataPersister.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public function onCreate($storyboard) {
3030
$section->column1 = $prototypeSection->column1;
3131
$section->column2 = $prototypeSection->column2;
3232
$section->column3 = $prototypeSection->column3;
33+
$section->setPos($prototypeSection->getPos());
3334

3435
$storyboard->addSection($section);
3536
}

api/src/Entity/ContentNode/MultiSelect.php

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,77 @@
55
use ApiPlatform\Core\Annotation\ApiProperty;
66
use ApiPlatform\Core\Annotation\ApiResource;
77
use App\Entity\ContentNode;
8+
use App\Repository\MultiSelectRepository;
89
use Doctrine\Common\Collections\ArrayCollection;
910
use Doctrine\Common\Collections\Collection;
1011
use Doctrine\ORM\Mapping as ORM;
12+
use Symfony\Component\Serializer\Annotation\Groups;
1113

1214
/**
13-
* @ORM\Entity
15+
* @ORM\Entity(repositoryClass=MultiSelectRepository::class)
1416
* @ORM\Table(name="content_node_multiselect")
15-
* @ApiResource(routePrefix="/content_node")]
1617
*/
18+
#[ApiResource(
19+
routePrefix: '/content_node',
20+
collectionOperations: [
21+
'get' => [
22+
'security' => 'is_fully_authenticated()',
23+
],
24+
'post' => [
25+
'denormalization_context' => ['groups' => ['write', 'create']],
26+
'security_post_denormalize' => 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)',
27+
'validation_groups' => ['Default', 'create'],
28+
],
29+
],
30+
itemOperations: [
31+
'get' => ['security' => 'is_granted("CAMP_COLLABORATOR", object) or is_granted("CAMP_IS_PROTOTYPE", object)'],
32+
'patch' => [
33+
'denormalization_context' => ['groups' => ['write', 'update']],
34+
'security' => 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)',
35+
'validation_groups' => ['Default', 'update'],
36+
],
37+
'delete' => ['security' => '(is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)) and object.owner === null'], // disallow delete when contentNode is a root node
38+
],
39+
denormalizationContext: ['groups' => ['write']],
40+
normalizationContext: ['groups' => ['read']],
41+
)]
1742
class MultiSelect extends ContentNode {
1843
/**
1944
* @ORM\OneToMany(targetEntity="MultiSelectOption", mappedBy="multiSelect", orphanRemoval=true, cascade={"persist"})
2045
*/
2146
#[ApiProperty(readableLink: true, writableLink: false)]
47+
#[Groups(['read'])]
2248
public Collection $options;
2349

2450
public function __construct() {
2551
$this->options = new ArrayCollection();
2652

2753
parent::__construct();
2854
}
55+
56+
/**
57+
* @return MultiSelectOption[]
58+
*/
59+
public function getOptions(): array {
60+
return $this->options->getValues();
61+
}
62+
63+
public function addOption(MultiSelectOption $option): self {
64+
if (!$this->options->contains($option)) {
65+
$this->options->add($option);
66+
$option->multiSelect = $this;
67+
}
68+
69+
return $this;
70+
}
71+
72+
public function removeOption(MultiSelectOption $option): self {
73+
if ($this->options->removeElement($option)) {
74+
if ($option->multiSelect === $this) {
75+
$option->multiSelect = null;
76+
}
77+
}
78+
79+
return $this;
80+
}
2981
}

api/src/Entity/ContentNode/MultiSelectOption.php

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,68 @@
22

33
namespace App\Entity\ContentNode;
44

5+
use ApiPlatform\Core\Annotation\ApiFilter;
56
use ApiPlatform\Core\Annotation\ApiProperty;
67
use ApiPlatform\Core\Annotation\ApiResource;
8+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
79
use App\Entity\BaseEntity;
10+
use App\Entity\BelongsToCampInterface;
11+
use App\Entity\Camp;
812
use App\Entity\SortableEntityInterface;
913
use App\Entity\SortableEntityTrait;
14+
use App\Repository\MultiSelectOptionRepository;
1015
use Doctrine\ORM\Mapping as ORM;
16+
use Symfony\Component\Serializer\Annotation\Groups;
1117

1218
/**
13-
* @ORM\Entity
19+
* @ORM\Entity(repositoryClass=MultiSelectOptionRepository::class)
1420
* @ORM\Table(name="content_node_multiselect_option")
15-
* @ApiResource(routePrefix="/content_node")]
1621
*/
17-
class MultiSelectOption extends BaseEntity implements SortableEntityInterface {
22+
#[ApiResource(
23+
routePrefix: '/content_node',
24+
collectionOperations: [
25+
'get' => [
26+
'security' => 'is_fully_authenticated()',
27+
],
28+
// 'post' => creating new options is not allowed
29+
],
30+
itemOperations: [
31+
'get' => ['security' => 'is_granted("CAMP_COLLABORATOR", object) or is_granted("CAMP_IS_PROTOTYPE", object)'],
32+
'patch' => [
33+
'denormalization_context' => ['groups' => ['write', 'update']],
34+
'security' => 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)',
35+
],
36+
// 'delete' => deleting single option is not allowed
37+
],
38+
denormalizationContext: ['groups' => ['write']],
39+
normalizationContext: ['groups' => ['read']],
40+
)]
41+
#[ApiFilter(SearchFilter::class, properties: ['multiSelect'])]
42+
class MultiSelectOption extends BaseEntity implements BelongsToCampInterface, SortableEntityInterface {
1843
use SortableEntityTrait;
1944

2045
/**
2146
* @ORM\ManyToOne(targetEntity="MultiSelect", inversedBy="options")
2247
* @ORM\JoinColumn(nullable=false, onDelete="cascade")
2348
*/
2449
#[ApiProperty(readableLink: false, writableLink: false)]
50+
#[Groups(['read'])]
2551
public MultiSelect $multiSelect;
2652

2753
/**
2854
* @ORM\Column(type="text", nullable=false)
2955
*/
56+
#[Groups(['read'])]
3057
public string $translateKey;
3158

3259
/**
3360
* @ORM\Column(type="boolean", nullable=false)
3461
*/
62+
#[Groups(['read', 'update'])]
3563
public bool $checked = false;
64+
65+
#[ApiProperty(readable: false)]
66+
public function getCamp(): ?Camp {
67+
return $this->multiSelect?->getCamp();
68+
}
3669
}

api/src/Entity/ContentNode/Storyboard.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
)]
4242
class Storyboard extends ContentNode {
4343
/**
44-
* @ORM\OneToMany(targetEntity="StoryboardSection", mappedBy="storyboard", orphanRemoval=true, cascade={"persist", "remove"})
44+
* @ORM\OneToMany(targetEntity="StoryboardSection", mappedBy="storyboard", orphanRemoval=true, cascade={"persist"})
4545
*/
4646
#[ApiProperty(readableLink: true, writableLink: false)]
4747
#[Groups(['read'])]

api/src/Entity/SortableEntityTrait.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22

33
namespace App\Entity;
44

5+
use Symfony\Component\Serializer\Annotation\Groups;
6+
57
trait SortableEntityTrait {
68
/**
79
* @ORM\Column(type="integer", nullable=false)
810
*/
911
private int $pos = 0;
1012

13+
#[Groups(['read'])]
1114
public function getPos(): int {
1215
return $this->pos;
1316
}
1417

18+
#[Groups(['write'])]
1519
public function setPos(int $pos): void {
1620
$this->pos = $pos;
1721
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\Repository;
4+
5+
use App\Entity\ContentNode\MultiSelectOption;
6+
use App\Entity\User;
7+
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
8+
use Doctrine\ORM\QueryBuilder;
9+
use Doctrine\Persistence\ManagerRegistry;
10+
11+
/**
12+
* @method null|MultiSelectOption find($id, $lockMode = null, $lockVersion = null)
13+
* @method null|MultiSelectOption findOneBy(array $criteria, array $orderBy = null)
14+
* @method MultiSelectOption[] findAll()
15+
* @method MultiSelectOption[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
16+
*/
17+
class MultiSelectOptionRepository extends ServiceEntityRepository implements CanFilterByUserInterface {
18+
use FiltersByContentNode;
19+
20+
public function __construct(ManagerRegistry $registry) {
21+
parent::__construct($registry, MultiSelectOption::class);
22+
}
23+
24+
public function filterByUser(QueryBuilder $queryBuilder, User $user): void {
25+
$rootAlias = $queryBuilder->getRootAliases()[0];
26+
$queryBuilder->innerJoin("{$rootAlias}.multiSelect", 'contentNode');
27+
28+
$this->filterByContentNode($queryBuilder, $user, 'contentNode');
29+
}
30+
}

0 commit comments

Comments
 (0)