Skip to content

Commit 9badc8f

Browse files
committed
ISSUE-345: create template endpoint
1 parent cb62975 commit 9badc8f

12 files changed

+519
-1
lines changed

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"friendsofsymfony/rest-bundle": "*",
3636
"symfony/test-pack": "^1.0",
3737
"symfony/process": "^6.4",
38-
"zircote/swagger-php": "^4.11"
38+
"zircote/swagger-php": "^4.11",
39+
"ext-dom": "*"
3940
},
4041
"require-dev": {
4142
"phpunit/phpunit": "^10.0",

config/services.yml

+3
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ services:
2121
PhpList\Core\Security\Authentication:
2222
autowire: true
2323
autoconfigure: true
24+
25+
GuzzleHttp\ClientInterface:
26+
class: GuzzleHttp\Client

config/services/managers.yml

+8
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,11 @@ services:
2323
PhpList\RestBundle\Service\Manager\MessageManager:
2424
autowire: true
2525
autoconfigure: true
26+
27+
PhpList\RestBundle\Service\Manager\TemplateManager:
28+
autowire: true
29+
autoconfigure: true
30+
31+
PhpList\RestBundle\Service\Manager\TemplateImageManager:
32+
autowire: true
33+
autoconfigure: true

config/services/validators.yml

+8
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,11 @@ services:
1818
autowire: true
1919
autoconfigure: true
2020
tags: [ 'validator.constraint_validator' ]
21+
22+
PhpList\RestBundle\Validator\TemplateLinkValidator:
23+
autowire: true
24+
autoconfigure: true
25+
26+
PhpList\RestBundle\Validator\TemplateImageValidator:
27+
autowire: true
28+
autoconfigure: true

src/Controller/TemplateController.php

+109
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
use PhpList\Core\Domain\Repository\Messaging\TemplateRepository;
1010
use PhpList\Core\Security\Authentication;
1111
use PhpList\RestBundle\Controller\Traits\AuthenticationTrait;
12+
use PhpList\RestBundle\Entity\Request\CreateTemplateRequest;
1213
use PhpList\RestBundle\Serializer\TemplateNormalizer;
14+
use PhpList\RestBundle\Service\Manager\TemplateManager;
15+
use PhpList\RestBundle\Validator\RequestValidator;
1316
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
1417
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
1518
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -29,15 +32,21 @@ class TemplateController extends AbstractController
2932

3033
private TemplateRepository $templateRepository;
3134
private TemplateNormalizer $normalizer;
35+
private RequestValidator $validator;
36+
private TemplateManager $templateManager;
3237

3338
public function __construct(
3439
Authentication $authentication,
3540
TemplateRepository $templateRepository,
3641
TemplateNormalizer $normalizer,
42+
RequestValidator $validator,
43+
TemplateManager $templateManager
3744
) {
3845
$this->authentication = $authentication;
3946
$this->templateRepository = $templateRepository;
4047
$this->normalizer = $normalizer;
48+
$this->validator = $validator;
49+
$this->templateManager = $templateManager;
4150
}
4251

4352
#[Route('', name: 'get_templates', methods: ['GET'])]
@@ -130,4 +139,104 @@ public function getTemplate(
130139

131140
return new JsonResponse($this->normalizer->normalize($template), Response::HTTP_OK);
132141
}
142+
143+
#[Route('', name: 'create_template', methods: ['POST'])]
144+
#[OA\Post(
145+
path: '/templates',
146+
description: 'Returns a JSON response of created template.',
147+
summary: 'Create a new template.',
148+
requestBody: new OA\RequestBody(
149+
description: 'Pass session credentials',
150+
required: true,
151+
content: new OA\MediaType(
152+
mediaType: 'multipart/form-data',
153+
schema: new OA\Schema(
154+
required: ['title'],
155+
properties: [
156+
new OA\Property(
157+
property: 'title',
158+
type: 'string',
159+
example: 'Newsletter Template'
160+
),
161+
new OA\Property(
162+
property: 'content',
163+
type: 'string',
164+
example: '<html><body>[CONTENT]</body></html>'
165+
),
166+
new OA\Property(
167+
property: 'text',
168+
type: 'string',
169+
example: '[CONTENT]'
170+
),
171+
new OA\Property(
172+
property: 'file',
173+
description: 'Optional file upload for HTML content',
174+
type: 'string',
175+
format: 'binary'
176+
),
177+
new OA\Property(
178+
property: 'check_links',
179+
description: 'Check that all links have full URLs',
180+
type: 'boolean',
181+
example: true
182+
),
183+
new OA\Property(
184+
property: 'check_images',
185+
description: 'Check that all images have full URLs',
186+
type: 'boolean',
187+
example: false
188+
),
189+
new OA\Property(
190+
property: 'check_external_images',
191+
description: 'Check that all external images exist',
192+
type: 'boolean',
193+
example: true
194+
),
195+
],
196+
type: 'object'
197+
)
198+
)
199+
),
200+
tags: ['templates'],
201+
parameters: [
202+
new OA\Parameter(
203+
name: 'session',
204+
description: 'Session ID obtained from authentication',
205+
in: 'header',
206+
required: true,
207+
schema: new OA\Schema(
208+
type: 'string'
209+
)
210+
)
211+
],
212+
responses: [
213+
new OA\Response(
214+
response: 201,
215+
description: 'Success',
216+
content: new OA\JsonContent(
217+
type: 'array',
218+
items: new OA\Items(ref: '#/components/schemas/Template')
219+
)
220+
),
221+
new OA\Response(
222+
response: 403,
223+
description: 'Failure',
224+
content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse')
225+
),
226+
new OA\Response(
227+
response: 422,
228+
description: 'Failure',
229+
content: new OA\JsonContent(ref: '#/components/schemas/ValidationErrorResponse')
230+
),
231+
]
232+
)]
233+
public function createTemplates(Request $request): JsonResponse
234+
{
235+
$this->requireAuthentication($request);
236+
237+
/** @var CreateTemplateRequest $createTemplateRequest */
238+
$createTemplateRequest = $this->validator->validate($request, CreateTemplateRequest::class);
239+
240+
return new JsonResponse($this->templateManager->create($createTemplateRequest), Response::HTTP_CREATED);
241+
}
133242
}

src/Entity/Dto/ValidationContext.php

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Entity\Dto;
6+
7+
class ValidationContext
8+
{
9+
private array $options = [];
10+
11+
public function set(string $key, mixed $value): self
12+
{
13+
$this->options[$key] = $value;
14+
15+
return $this;
16+
}
17+
18+
public function get(string $key, mixed $default = null): mixed
19+
{
20+
return $this->options[$key] ?? $default;
21+
}
22+
23+
public function has(string $key): bool
24+
{
25+
return array_key_exists($key, $this->options);
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Entity\Request;
6+
7+
use Symfony\Component\HttpFoundation\File\UploadedFile;
8+
use Symfony\Component\Validator\Constraints as Assert;
9+
10+
class CreateTemplateRequest
11+
{
12+
#[Assert\NotBlank]
13+
public string $title;
14+
15+
#[Assert\NotBlank]
16+
public string $content;
17+
18+
public ?string $text = null;
19+
20+
public ?UploadedFile $file = null;
21+
public bool $checkLinks = false;
22+
public bool $checkImages = false;
23+
public bool $checkExternalImages = false;
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Service\Manager;
6+
7+
use Doctrine\ORM\EntityManagerInterface;
8+
use DOMDocument;
9+
use PhpList\Core\Domain\Model\Messaging\Template;
10+
use PhpList\Core\Domain\Model\Messaging\TemplateImage;
11+
use PhpList\Core\Domain\Repository\Messaging\TemplateImageRepository;
12+
13+
class TemplateImageManager
14+
{
15+
public const IMAGE_MIME_TYPES = [
16+
'gif' => 'image/gif',
17+
'jpg' => 'image/jpeg',
18+
'jpeg' => 'image/jpeg',
19+
'jpe' => 'image/jpeg',
20+
'bmp' => 'image/bmp',
21+
'png' => 'image/png',
22+
'tif' => 'image/tiff',
23+
'tiff' => 'image/tiff',
24+
'swf' => 'application/x-shockwave-flash',
25+
];
26+
27+
private TemplateImageRepository $templateImageRepository;
28+
private EntityManagerInterface $entityManager;
29+
30+
public function __construct(
31+
TemplateImageRepository $templateImageRepository,
32+
EntityManagerInterface $entityManager
33+
) {
34+
$this->templateImageRepository = $templateImageRepository;
35+
$this->entityManager = $entityManager;
36+
}
37+
38+
/** @return TemplateImage[] */
39+
public function createImagesFromImagePaths(array $imagePaths, Template $template): array
40+
{
41+
$templateImages = [];
42+
foreach ($imagePaths as $path) {
43+
$image = new TemplateImage();
44+
$image->setTemplate($template);
45+
$image->setFilename($path);
46+
$image->setMimeType($this->guessMimeType($path));
47+
$image->setData(null);
48+
49+
$this->entityManager->persist($image);
50+
$templateImages[] = $image;
51+
}
52+
53+
$this->entityManager->flush();
54+
55+
return $templateImages;
56+
}
57+
58+
private function guessMimeType(string $filename): string
59+
{
60+
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
61+
return self::IMAGE_MIME_TYPES[$ext] ?? 'application/octet-stream';
62+
}
63+
64+
public function extractAllImages(string $html): array
65+
{
66+
$fromRegex = array_keys(
67+
$this->extractTemplateImagesFromContent($html)
68+
);
69+
70+
$fromDom = $this->extractImagesFromHtml($html);
71+
72+
return array_values(array_unique(array_merge($fromRegex, $fromDom)));
73+
}
74+
75+
private function extractTemplateImagesFromContent(string $content): array
76+
{
77+
$regexp = sprintf('/"([^"]+\.(%s))"/Ui', implode('|', array_keys(self::IMAGE_MIME_TYPES)));
78+
preg_match_all($regexp, stripslashes($content), $images);
79+
80+
return array_count_values($images[1]);
81+
}
82+
83+
private function extractImagesFromHtml(string $html): array
84+
{
85+
$dom = new DOMDocument();
86+
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
87+
@$dom->loadHTML($html);
88+
$images = [];
89+
90+
foreach ($dom->getElementsByTagName('img') as $img) {
91+
$src = $img->getAttribute('src');
92+
if ($src) {
93+
$images[] = $src;
94+
}
95+
}
96+
97+
return $images;
98+
}
99+
100+
public function delete(TemplateImage $templateImage): void
101+
{
102+
$this->templateImageRepository->remove($templateImage);
103+
}
104+
}

0 commit comments

Comments
 (0)