Passed
Push — trunk ( d12826...fb7a56 )
by Christian
13:45 queued 14s
created

SeoActionController::updateCanonicalUrl()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 46
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 26
nc 7
nop 2
dl 0
loc 46
rs 8.5706
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\Content\Seo\Api;
4
5
use Shopware\Core\Content\Seo\Exception\NoEntitiesForPreviewException;
6
use Shopware\Core\Content\Seo\SeoException;
7
use Shopware\Core\Content\Seo\SeoUrl\SeoUrlEntity;
8
use Shopware\Core\Content\Seo\SeoUrlGenerator;
9
use Shopware\Core\Content\Seo\SeoUrlPersister;
10
use Shopware\Core\Content\Seo\SeoUrlRoute\SeoUrlRouteConfig;
11
use Shopware\Core\Content\Seo\SeoUrlRoute\SeoUrlRouteRegistry;
12
use Shopware\Core\Content\Seo\Validation\SeoUrlDataValidationFactoryInterface;
13
use Shopware\Core\Content\Seo\Validation\SeoUrlValidationFactory;
14
use Shopware\Core\Defaults;
15
use Shopware\Core\Framework\Context;
16
use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
17
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
18
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
19
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
20
use Shopware\Core\Framework\DataAbstractionLayer\Search\RequestCriteriaBuilder;
21
use Shopware\Core\Framework\Feature;
22
use Shopware\Core\Framework\Log\Package;
23
use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
24
use Shopware\Core\Framework\Validation\DataValidator;
25
use Shopware\Core\System\SalesChannel\SalesChannelCollection;
26
use Shopware\Core\System\SalesChannel\SalesChannelEntity;
27
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
28
use Symfony\Component\HttpFoundation\JsonResponse;
29
use Symfony\Component\HttpFoundation\ParameterBag;
30
use Symfony\Component\HttpFoundation\Request;
31
use Symfony\Component\HttpFoundation\Response;
32
use Symfony\Component\Routing\Annotation\Route;
33
34
#[Route(defaults: ['_routeScope' => ['api']])]
35
#[Package('buyers-experience')]
36
class SeoActionController extends AbstractController
37
{
38
    /**
39
     * @internal
40
     */
41
    public function __construct(
42
        private readonly SeoUrlGenerator $seoUrlGenerator,
43
        private readonly SeoUrlPersister $seoUrlPersister,
44
        private readonly DefinitionInstanceRegistry $definitionRegistry,
45
        private readonly SeoUrlRouteRegistry $seoUrlRouteRegistry,
46
        private readonly SeoUrlDataValidationFactoryInterface $seoUrlValidator,
47
        private readonly DataValidator $validator,
48
        private readonly EntityRepository $salesChannelRepository,
49
        private readonly RequestCriteriaBuilder $requestCriteriaBuilder,
50
        private readonly DefinitionInstanceRegistry $definitionInstanceRegistry
51
    ) {
52
    }
53
54
    #[Route(path: '/api/_action/seo-url-template/validate', name: 'api.seo-url-template.validate', methods: ['POST'])]
55
    public function validate(Request $request, Context $context): JsonResponse
56
    {
57
        $context->setConsiderInheritance(true);
58
59
        $this->validateSeoUrlTemplate($request);
60
        $seoUrlTemplate = $request->request->all();
61
62
        // just call it to validate the template
63
        $this->getPreview($seoUrlTemplate, $context);
64
65
        return new JsonResponse();
66
    }
67
68
    #[Route(path: '/api/_action/seo-url-template/preview', name: 'api.seo-url-template.preview', methods: ['POST'])]
69
    public function preview(Request $request, Context $context): Response
70
    {
71
        $this->validateSeoUrlTemplate($request);
72
        $seoUrlTemplate = $request->request->all();
73
74
        $previewCriteria = new Criteria();
75
        if (\array_key_exists('criteria', $seoUrlTemplate) && \is_string($seoUrlTemplate['entityName']) && \is_array($seoUrlTemplate['criteria'])) {
76
            $definition = $this->definitionInstanceRegistry->getByEntityName($seoUrlTemplate['entityName']);
77
78
            $previewCriteria = $this->requestCriteriaBuilder->handleRequest(
79
                Request::create('', 'POST', $seoUrlTemplate['criteria']),
80
                $previewCriteria,
81
                $definition,
82
                $context
83
            );
84
            unset($seoUrlTemplate['criteria']);
85
        }
86
87
        try {
88
            $preview = $this->getPreview($seoUrlTemplate, $context, $previewCriteria);
89
        } catch (NoEntitiesForPreviewException) {
90
            return new Response('', Response::HTTP_NO_CONTENT);
91
        }
92
93
        return new JsonResponse($preview);
94
    }
95
96
    #[Route(path: '/api/_action/seo-url-template/context', name: 'api.seo-url-template.context', methods: ['POST'])]
97
    public function getSeoUrlContext(RequestDataBag $data, Context $context): JsonResponse
98
    {
99
        $routeName = $data->get('routeName');
100
        $fk = $data->get('foreignKey');
101
        $seoUrlRoute = $this->seoUrlRouteRegistry->findByRouteName($routeName);
102
        if (!$seoUrlRoute) {
103
            throw SeoException::seoUrlRouteNotFound($routeName);
104
        }
105
106
        $config = $seoUrlRoute->getConfig();
107
        $repository = $this->getRepository($config);
108
109
        $criteria = new Criteria();
110
        if (!empty($fk)) {
111
            $criteria = new Criteria([$fk]);
112
        }
113
        $criteria->setLimit(1);
114
115
        $entity = $repository
116
            ->search($criteria, $context)
117
            ->first();
118
119
        if (!$entity) {
120
            return new JsonResponse(null, Response::HTTP_NOT_FOUND);
121
        }
122
123
        $mapping = $seoUrlRoute->getMapping($entity, null);
124
125
        return new JsonResponse($mapping->getSeoPathInfoContext());
126
    }
127
128
    #[Route(path: '/api/_action/seo-url/canonical', name: 'api.seo-url.canonical', methods: ['PATCH'])]
129
    public function updateCanonicalUrl(RequestDataBag $seoUrl, Context $context): Response
130
    {
131
        if (!$seoUrl->has('routeName')) {
132
            throw SeoException::routeNameParameterIsMissing();
133
        }
134
135
        $seoUrlRoute = $this->seoUrlRouteRegistry->findByRouteName($seoUrl->get('routeName') ?? '');
136
        if (!$seoUrlRoute) {
137
            throw SeoException::seoUrlRouteNotFound($seoUrl->get('routeName'));
138
        }
139
140
        $validation = $this->seoUrlValidator->buildValidation($context, $seoUrlRoute->getConfig());
141
142
        $seoUrlData = $seoUrl->all();
143
        $this->validator->validate($seoUrlData, $validation);
144
        $seoUrlData['isModified'] ??= true;
145
146
        $salesChannelId = $seoUrlData['salesChannelId'] ?? null;
147
148
        if ($salesChannelId === null) {
149
            throw SeoException::salesChannelIdParameterIsMissing();
150
        }
151
152
        /** @var SalesChannelEntity|null $salesChannel */
153
        $salesChannel = $this->salesChannelRepository->search(new Criteria([$salesChannelId]), $context)->first();
154
155
        if ($salesChannel === null) {
156
            throw SeoException::salesChannelNotFound($salesChannelId);
157
        }
158
159
        if ($salesChannel->getTypeId() === Defaults::SALES_CHANNEL_TYPE_API) {
160
            if (Feature::isActive('v6.6.0.0')) {
161
                return new Response('', Response::HTTP_NO_CONTENT);
162
            }
163
        }
164
165
        $this->seoUrlPersister->updateSeoUrls(
166
            $context,
167
            $seoUrlData['routeName'],
168
            [$seoUrlData['foreignKey']],
169
            [$seoUrlData],
170
            $salesChannel
171
        );
172
173
        return new Response('', Response::HTTP_NO_CONTENT);
174
    }
175
176
    #[Route(path: '/api/_action/seo-url/create-custom-url', name: 'api.seo-url.create', methods: ['POST'])]
177
    public function createCustomSeoUrls(RequestDataBag $dataBag, Context $context): Response
178
    {
179
        /** @var ParameterBag $dataBag */
180
        $dataBag = $dataBag->get('urls');
181
        $urls = $dataBag->all();
182
183
        /** @var SeoUrlValidationFactory $validatorBuilder */
184
        $validatorBuilder = $this->seoUrlValidator;
185
186
        $validation = $validatorBuilder->buildValidation($context, null);
187
        $salesChannels = new SalesChannelCollection();
188
189
        $salesChannelIds = array_column($urls, 'salesChannelId');
190
191
        if (!empty($salesChannelIds)) {
192
            $salesChannels = $this->salesChannelRepository->search(new Criteria($salesChannelIds), $context)->getEntities();
193
        }
194
195
        $writeData = [];
196
197
        foreach ($urls as $seoUrlData) {
198
            $id = $seoUrlData['salesChannelId'] ?? null;
199
200
            $this->validator->validate($seoUrlData, $validation);
201
            $seoUrlData['isModified'] ??= true;
202
203
            $writeData[$id][] = $seoUrlData;
204
        }
205
206
        foreach ($writeData as $salesChannelId => $writeRows) {
207
            if ($salesChannelId === '') {
208
                throw SeoException::salesChannelIdParameterIsMissing();
209
            }
210
211
            /** @var SalesChannelEntity $salesChannelEntity */
212
            $salesChannelEntity = $salesChannels->get($salesChannelId);
213
214
            if ($salesChannelEntity === null) {
215
                throw SeoException::salesChannelNotFound((string) $salesChannelId);
216
            }
217
218
            if ($salesChannelEntity->getTypeId() === Defaults::SALES_CHANNEL_TYPE_API) {
219
                if (Feature::isActive('v6.6.0.0')) {
220
                    continue;
221
                }
222
            }
223
224
            $this->seoUrlPersister->updateSeoUrls(
225
                $context,
226
                $writeRows[0]['routeName'],
227
                array_column($writeRows, 'foreignKey'),
228
                $writeRows,
229
                $salesChannelEntity
230
            );
231
        }
232
233
        return new Response('', Response::HTTP_NO_CONTENT);
234
    }
235
236
    #[Route(path: '/api/_action/seo-url-template/default/{routeName}', name: 'api.seo-url-template.default', methods: ['GET'])]
237
    public function getDefaultSeoTemplate(string $routeName, Context $context): JsonResponse
238
    {
239
        $seoUrlRoute = $this->seoUrlRouteRegistry->findByRouteName($routeName);
240
241
        if (!$seoUrlRoute) {
242
            throw SeoException::seoUrlRouteNotFound($routeName);
243
        }
244
245
        return new JsonResponse(['defaultTemplate' => $seoUrlRoute->getConfig()->getTemplate()]);
246
    }
247
248
    private function validateSeoUrlTemplate(Request $request): void
249
    {
250
        if (!$request->request->has('template')) {
251
            throw SeoException::templateParameterIsMissing();
252
        }
253
254
        if (!$request->request->has('salesChannelId')) {
255
            throw SeoException::salesChannelIdParameterIsMissing();
256
        }
257
258
        if (!$request->request->has('routeName')) {
259
            throw SeoException::routeNameParameterIsMissing();
260
        }
261
262
        if (!$request->request->has('entityName')) {
263
            throw SeoException::entityNameParameterIsMissing();
264
        }
265
    }
266
267
    /**
268
     * @param array<string, mixed> $seoUrlTemplate
269
     *
270
     * @return array<SeoUrlEntity>
271
     */
272
    private function getPreview(array $seoUrlTemplate, Context $context, ?Criteria $previewCriteria = null): array
273
    {
274
        $seoUrlRoute = $this->seoUrlRouteRegistry->findByRouteName($seoUrlTemplate['routeName']);
275
276
        if (!$seoUrlRoute) {
277
            throw SeoException::seoUrlRouteNotFound($seoUrlTemplate['routeName']);
278
        }
279
280
        $config = $seoUrlRoute->getConfig();
281
        $config->setSkipInvalid(false);
282
        $repository = $this->getRepository($config);
283
284
        $criteria = new Criteria();
285
        if ($previewCriteria !== null) {
286
            $criteria = $previewCriteria;
287
        }
288
        $criteria->setLimit(10);
289
290
        $ids = $repository->searchIds($criteria, $context)->getIds();
291
292
        if (empty($ids)) {
293
            throw SeoException::noEntitiesForPreview($repository->getDefinition()->getEntityName(), $seoUrlTemplate['routeName']);
294
        }
295
296
        $salesChannelId = $seoUrlTemplate['salesChannelId'] ?? null;
297
        $template = $seoUrlTemplate['template'] ?? '';
298
299
        if (\is_string($salesChannelId)) {
300
            /** @var SalesChannelEntity|null $salesChannel */
301
            $salesChannel = $this->salesChannelRepository->search((new Criteria([$salesChannelId]))->setLimit(1), $context)->get($salesChannelId);
302
303
            if ($salesChannel === null) {
304
                throw SeoException::invalidSalesChannelId($salesChannelId);
305
            }
306
        } else {
307
            /** @var SalesChannelEntity|null $salesChannel */
308
            $salesChannel = $this->salesChannelRepository
309
                ->search(
310
                    (new Criteria())->addFilter(new EqualsFilter('typeId', Defaults::SALES_CHANNEL_TYPE_STOREFRONT))->setLimit(1),
311
                    $context
312
                )
313
                ->first();
314
        }
315
316
        if ($salesChannel === null) {
317
            throw SeoException::salesChannelIdParameterIsMissing();
318
        }
319
320
        $result = $this->seoUrlGenerator->generate($ids, $template, $seoUrlRoute, $context, $salesChannel);
321
        if (\is_array($result)) {
322
            return $result;
323
        }
324
325
        return iterator_to_array($result);
326
    }
327
328
    private function getRepository(SeoUrlRouteConfig $config): EntityRepository
329
    {
330
        return $this->definitionRegistry->getRepository($config->getDefinition()->getEntityName());
331
    }
332
}
333