Skip to content

Commit 4f82ba0

Browse files
committed
add PUT to upsert multiple communities
1 parent 6e60a28 commit 4f82ba0

File tree

12 files changed

+512
-4
lines changed

12 files changed

+512
-4
lines changed

compose.yaml

+16-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ services:
3939
- app-network
4040
ports:
4141
- 9200:9200
42-
4342
backend:
4443
extra_hosts: *default-extra_hosts
4544
container_name: openchurch_backend
@@ -63,7 +62,7 @@ services:
6362
APP_ENV: dev
6463
APP_SECRET: "secret"
6564
DATABASE_URL: mysql://root:openchurch@db:3306/openchurch?serverVersion=11.5.2-MariaDB&charset=utf8mb4
66-
HOST_API: api.openchurch.local/api
65+
HOST_API: https://api.openchurch.local/api
6766
HOST_ADMIN: admin.openchurch.local
6867
ELASTIC_PASSWORD: admin
6968
ELASTICSEARCH_IRI: https://elastic:admin@elasticsearch:9200
@@ -73,6 +72,8 @@ services:
7372
build:
7473
context: .
7574
dockerfile: ./docker/python/Dockerfile
75+
depends_on:
76+
- redis
7677
networks:
7778
- app-network
7879
extra_hosts:
@@ -84,6 +85,19 @@ services:
8485
SYNCHRO_SECRET_KEY: "secret"
8586
volumes:
8687
- ./scripts:/app
88+
redis:
89+
image: redis:5
90+
container_name: openchurch_redis
91+
expose:
92+
- 6379
93+
networks:
94+
- app-network
95+
healthcheck:
96+
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
97+
interval: 1s
98+
timeout: 5s
99+
retries: 55
100+
start_period: 30s
87101

88102
volumes:
89103
openchurch_db_data: {}

src/Community/Domain/Repository/CommunityRepositoryInterface.php

+2
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,7 @@ public function withType(string $type): static;
2828

2929
public function withWikidataId(int $wikidataId): static;
3030

31+
public function withWikidataIds(array $wikidataIds): static;
32+
3133
public function withParentCommunityId(Uuid $parentId): static;
3234
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Community\Infrastructure\ApiPlatform\Input;
6+
7+
final class CommunityFieldsInput {
8+
9+
/**
10+
* @param array<Field[]> $fields
11+
*/
12+
public array $wikidataEntities = [];
13+
}

src/Community/Infrastructure/ApiPlatform/Resource/CommunityResource.php

+11
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@
88
use ApiPlatform\Metadata\ApiResource;
99
use ApiPlatform\Metadata\Get;
1010
use ApiPlatform\Metadata\GetCollection;
11+
use ApiPlatform\Metadata\Link;
1112
use ApiPlatform\Metadata\Patch;
1213
use ApiPlatform\Metadata\Post;
14+
use ApiPlatform\Metadata\Put;
1315
use App\Community\Domain\Model\Community;
16+
use App\Community\Infrastructure\ApiPlatform\Input\CommunityFieldsInput;
1417
use App\Community\Infrastructure\ApiPlatform\State\Processor\CreateCommunityProcessor;
1518
use App\Community\Infrastructure\ApiPlatform\State\Processor\UpdateCommunityProcessor;
19+
use App\Community\Infrastructure\ApiPlatform\State\Processor\UpsertCommunityProcessor;
1620
use App\Community\Infrastructure\ApiPlatform\State\Provider\CommunityCollectionProvider;
1721
use App\Community\Infrastructure\ApiPlatform\State\Provider\CommunityItemProvider;
1822
use App\Field\Domain\Model\Field;
@@ -44,6 +48,13 @@
4448
processor: UpdateCommunityProcessor::class,
4549
normalizationContext: ['groups' => ['communities']],
4650
),
51+
new Put(
52+
security: 'is_granted("ROLE_AGENT")',
53+
uriTemplate: '/communities/upsert',
54+
status: 200,
55+
processor: UpsertCommunityProcessor::class,
56+
input: CommunityFieldsInput::class,
57+
),
4758
new GetCollection(
4859
filters: [
4960
FieldTypeFilter::class,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Community\Infrastructure\ApiPlatform\State\Processor;
6+
7+
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
8+
use ApiPlatform\Metadata\Operation;
9+
use ApiPlatform\State\ProcessorInterface;
10+
use ApiPlatform\Validator\Exception\ValidationException;
11+
use App\Community\Domain\Model\Community;
12+
use App\Community\Domain\Repository\CommunityRepositoryInterface;
13+
use App\Community\Infrastructure\ApiPlatform\Input\CommunityFieldsInput;
14+
use App\Field\Application\FieldService;
15+
use App\Field\Domain\Enum\FieldCommunity;
16+
use App\Field\Domain\Exception\FieldWikidataIdMissingException;
17+
use App\Field\Domain\Model\Field;
18+
use App\Shared\Domain\Manager\TransactionManagerInterface;
19+
use Exception;
20+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
21+
use Symfony\Component\Uid\UuidV7;
22+
use Symfony\Contracts\HttpClient\HttpClientInterface;
23+
use Webmozart\Assert\Assert;
24+
25+
use function Zenstruck\Foundry\Persistence\delete;
26+
27+
/**
28+
* @implements ProcessorInterface<CommunityFieldsInput, CommunityFieldsInput>
29+
*/
30+
final class UpsertCommunityProcessor implements ProcessorInterface
31+
{
32+
public function __construct(
33+
private CommunityRepositoryInterface $communityRepo,
34+
private TransactionManagerInterface $transactionManager,
35+
private HttpClientInterface $httpClient,
36+
private UpdateCommunityProcessor $updateCommunityProcessor,
37+
private FieldService $fieldService,
38+
private DenormalizerInterface $denormalizer,
39+
) {
40+
}
41+
42+
/**
43+
* @param CommunityFieldsInput $data
44+
*/
45+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): array
46+
{
47+
return $this->transactionManager->transactional(function () use ($data) {
48+
Assert::isInstanceOf($data, CommunityFieldsInput::class);
49+
50+
$wikidataIdFields = [];
51+
$result = [];
52+
53+
$wikidataIds = array_map(function (array $fields) use(&$wikidataIdFields) {
54+
$wikidataField = $this->getFieldByName($fields, FieldCommunity::WIKIDATA_ID->value);
55+
if (!$wikidataField) throw new FieldWikidataIdMissingException();
56+
57+
$wikidataId = $wikidataField['value'];
58+
$wikidataIdFields[$wikidataId] = $fields;
59+
return $wikidataId;
60+
}, $data->wikidataEntities);
61+
62+
// Update...
63+
$communities = $this->communityRepo->addSelectField()->withWikidataIds($wikidataIds)->asCollection();
64+
foreach ($communities as $community) {
65+
$wikidataId = $community->getMostTrustableFieldByName(FieldCommunity::WIKIDATA_ID)->getValue();
66+
$openChurchWikidataUpdatedAt = $community->getMostTrustableFieldByName(FieldCommunity::WIKIDATA_UPDATED_AT);
67+
$wikidataUpdatedAt = $this->getFieldByName($wikidataIdFields[$wikidataId], FieldCommunity::WIKIDATA_UPDATED_AT->value);
68+
69+
if (!$openChurchWikidataUpdatedAt || $wikidataUpdatedAt['value'] !== $openChurchWikidataUpdatedAt->getValue()) {
70+
// WikidataUpdatedAt is not the same. We have to update the data
71+
try {
72+
$this->fieldService->upsertFields($community, $this->arrayToFields($wikidataIdFields[$wikidataId]));
73+
$result[$wikidataId] = 'Updated';
74+
}
75+
catch (Exception $e) {
76+
$result[$wikidataId] = $this->handleError($e);
77+
}
78+
}
79+
unset($wikidataIdFields[$wikidataId]);
80+
}
81+
82+
// Insert...
83+
foreach ($wikidataIdFields as $wikidataId => $fields) {
84+
try {
85+
$community = new Community();
86+
$this->communityRepo->add($community);
87+
$this->fieldService->upsertFields($community, $this->arrayToFields($fields));
88+
$result[$wikidataId] = 'Inserted';
89+
}
90+
catch (Exception $e) {
91+
$result[$wikidataId] = $this->handleError($e);
92+
}
93+
}
94+
95+
return $result;
96+
});
97+
}
98+
99+
private function arrayToFields(array $fields) : array {
100+
return array_map(
101+
fn (array $field) => $this->denormalizer->denormalize($field, Field::class),
102+
$fields
103+
);
104+
}
105+
106+
private function getFieldByName(array $fields, string $fieldName) : mixed {
107+
$result = array_values(array_filter($fields,
108+
fn (array $field) => $field['name'] === $fieldName
109+
));
110+
111+
if (count($result) > 0) {
112+
return $result[0];
113+
}
114+
115+
return null;
116+
}
117+
118+
private function handleError(Exception $e) : string {
119+
$this->communityRepo->clear();
120+
if ($e instanceof ValidationException) {
121+
return $e->getMessage();
122+
}
123+
if ($e instanceof ProblemExceptionInterface) {
124+
return $e->getDetail();
125+
}
126+
127+
return $e->getMessage();
128+
}
129+
}

src/Community/Infrastructure/ApiPlatform/State/Provider/CommunityCollectionProvider.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
3636
/** @var string|null $type */
3737
$type = $context['filters'][FieldCommunity::TYPE->value] ?? null;
3838
$wikidataId = $context['filters'][FieldCommunity::WIKIDATA_ID->value] ?? null;
39+
3940
$name = $context['filters'][FieldCommunity::NAME->value] ?? null;
4041
$page = $itemsPerPage = null;
4142

@@ -49,6 +50,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
4950
if (!$type) {
5051
throw new CommunityTypeNotProvidedException();
5152
}
53+
5254
$entityIds = match ($type) {
5355
CommunityType::PARISH->value => $this->searchService->searchParishIds($name, $itemsPerPage, $page - 1),
5456
CommunityType::DIOCESE->value => $this->searchService->searchDioceseIds($name, $itemsPerPage, $page - 1),
@@ -63,7 +65,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
6365
$models = $this->communityRepo
6466
->ofIds(array_map(fn (string $entityId) => Uuid::fromString($entityId), $entityIds ?? []))
6567
->withType($type)
66-
->withWikidataId((int) $wikidataId)
68+
->withWikidataId(intval($wikidataId))
6769
->withPagination($page, $itemsPerPage);
6870

6971
$resources = [];

src/Community/Infrastructure/Doctrine/DoctrineCommunityRepository.php

+17
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,23 @@ public function withWikidataId(?int $value): static
9090
});
9191
}
9292

93+
public function withWikidataIds(?array $wikidataIds): static
94+
{
95+
if (count($wikidataIds) === 0) {
96+
return $this;
97+
}
98+
99+
return
100+
$this->filter(static function (QueryBuilder $qb) use ($wikidataIds): void {
101+
$qb->andWhere("
102+
EXISTS (SELECT 1 FROM App\Field\Domain\Model\Field f_wikidata
103+
WHERE f_wikidata.community = community
104+
AND f_wikidata.name = 'wikidataId' AND f_wikidata.intVal IN(:valueWikidataIds))
105+
")
106+
->setParameter('valueWikidataIds', $wikidataIds);
107+
});
108+
}
109+
93110
public function withParentCommunityId(?Uuid $parentId): static
94111
{
95112
if (!$parentId) {

src/Field/Domain/Enum/FieldCommunity.php

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ enum FieldCommunity: string
2323
self::CONTACT_COUNTRY_CODE->value => Types::STRING,
2424
self::MESSESINFO_ID->value => Types::STRING,
2525
self::WIKIDATA_ID->value => Types::INTEGER,
26+
self::WIKIDATA_UPDATED_AT->value => Types::STRING,
2627
self::PARENT_COMMUNITY_ID->value => 'Community',
2728
self::REPLACES->value => 'Community[]',
2829
];
@@ -40,6 +41,7 @@ enum FieldCommunity: string
4041
case CONTACT_COUNTRY_CODE = 'contactCountryCode';
4142
case MESSESINFO_ID = 'messesInfoId';
4243
case WIKIDATA_ID = 'wikidataId';
44+
case WIKIDATA_UPDATED_AT = 'wikidataUpdatedAt';
4345
case PARENT_COMMUNITY_ID = 'parentCommunityId';
4446
case REPLACES = 'replaces';
4547

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Field\Domain\Exception;
6+
7+
use ApiPlatform\Metadata\ErrorResource;
8+
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
9+
use Symfony\Component\Serializer\Annotation\Groups;
10+
11+
#[ErrorResource]
12+
class FieldWikidataIdMissingException extends \Exception implements ProblemExceptionInterface
13+
{
14+
#[Groups(['communities', 'places'])]
15+
public function getType(): string
16+
{
17+
return 'FieldWikidataIdMissingException';
18+
}
19+
20+
#[Groups(['communities', 'places'])]
21+
public function getTitle(): ?string
22+
{
23+
return 'field wikidataId is missing';
24+
}
25+
26+
#[Groups(['communities', 'places'])]
27+
public function getStatus(): ?int
28+
{
29+
return 400;
30+
}
31+
32+
#[Groups(['communities', 'places'])]
33+
public function getDetail(): ?string
34+
{
35+
return sprintf('Field wikidataId is missing');
36+
}
37+
38+
/**
39+
* @codeCoverageIgnore
40+
*/
41+
public function getInstance(): ?string
42+
{
43+
return null;
44+
}
45+
}

src/Place/Infrastructure/ApiPlatform/Resource/PlaceResource.php

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

2020
#[ApiResource(
2121
shortName: 'Place',
22+
cacheHeaders: [
23+
'public' => true,
24+
'max_age' => 3600,
25+
],
2226
operations: [
2327
new Post(
2428
security: 'is_granted("ROLE_AGENT")',
@@ -37,6 +41,7 @@
3741
),
3842
new Get(
3943
provider: PlaceItemProvider::class,
44+
normalizationContext: ['groups' => ['places']],
4045
),
4146
],
4247
)]

0 commit comments

Comments
 (0)