]> Raphaƫl G. Git Repositories - airbundle/blob - Controller/AbstractController.php
Add strict type
[airbundle] / Controller / AbstractController.php
1 <?php
2
3 namespace Rapsys\AirBundle\Controller;
4
5 use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
6 use Symfony\Component\Filesystem\Filesystem;
7 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
8 use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait;
9 use Symfony\Component\HttpFoundation\Response;
10 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
11
12 use Rapsys\AirBundle\Entity\Slot;
13 use Rapsys\UserBundle\Utils\Slugger;
14
15 /**
16 *
17 * Provides common features needed in controllers.
18 *
19 * {@inheritdoc}
20 */
21 abstract class AbstractController extends BaseAbstractController {
22 use ControllerTrait {
23 //Rename render as baseRender
24 render as protected baseRender;
25 }
26
27 /**
28 * Return the facebook image
29 *
30 * @desc Generate image in jpeg format or load it from cache
31 *
32 * @param string $pathInfo The request path info
33 * @param array $parameters The image parameters
34 * @return array The image array
35 */
36 protected function getFacebookImage(string $pathInfo, array $parameters = []): array {
37 //Get asset package
38 //XXX: require asset package to be public
39 $package = $this->container->get('rapsys_pack.path_package');
40
41 //Set texts
42 $texts = $parameters['texts'] ?? [];
43
44 //Set default source
45 $source = $parameters['source'] ?? 'png/facebook.png';
46
47 //Set default source
48 $updated = $parameters['updated'] ?? strtotime('last week');
49
50 //Set source path
51 $src = $this->config['path']['public'].'/'.$source;
52
53 //Set cache path
54 //XXX: remove extension and store as png anyway
55 $cache = $this->config['path']['cache'].'/facebook/'.substr($source, 0, strrpos($source, '.')).'.'.$this->config['facebook']['width'].'x'.$this->config['facebook']['height'].'.png';
56
57 //Set destination path
58 //XXX: format <public>/facebook<pathinfo>.jpeg
59 //XXX: was <public>/facebook/<controller>/<action>.<locale>.jpeg
60 $dest = $this->config['path']['public'].'/facebook'.$pathInfo.'.jpeg';
61
62 //With up to date generated image
63 if (
64 is_file($dest) &&
65 ($stat = stat($dest)) &&
66 $stat['mtime'] >= $updated
67 ) {
68 //Get image size
69 list ($width, $height) = getimagesize($dest);
70
71 //Iterate each text
72 foreach($texts as $text => $data) {
73 //With canonical text
74 if (!empty($data['canonical'])) {
75 //Prevent canonical to finish in alt
76 unset($texts[$text]);
77 }
78 }
79
80 //Return image data
81 return [
82 'og:image' => $package->getAbsoluteUrl('@RapsysAir/facebook/'.$stat['mtime'].$pathInfo.'.jpeg'),
83 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
84 'og:image:height' => $height,
85 'og:image:width' => $width
86 ];
87 //With image candidate
88 } elseif (is_file($src)) {
89 //Create image object
90 $image = new \Imagick();
91
92 //With cache image
93 if (is_file($cache)) {
94 //Read image
95 $image->readImage($cache);
96 //Without we generate it
97 } else {
98 //Check target directory
99 if (!is_dir($dir = dirname($cache))) {
100 //Create filesystem object
101 $filesystem = new Filesystem();
102
103 try {
104 //Create dir
105 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
106 $filesystem->mkdir($dir, 0775);
107 } catch (IOExceptionInterface $e) {
108 //Throw error
109 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
110 }
111 }
112
113 //Read image
114 $image->readImage($src);
115
116 //Crop using aspect ratio
117 //XXX: for better result upload image directly in aspect ratio :)
118 $image->cropThumbnailImage($this->config['facebook']['width'], $this->config['facebook']['height']);
119
120 //Strip image exif data and properties
121 $image->stripImage();
122
123 //Save cache image
124 if (!$image->writeImage($cache)) {
125 //Throw error
126 throw new \Exception(sprintf('Unable to write image "%s"', $cache));
127 }
128 }
129 //Check target directory
130 if (!is_dir($dir = dirname($dest))) {
131 //Create filesystem object
132 $filesystem = new Filesystem();
133
134 try {
135 //Create dir
136 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
137 $filesystem->mkdir($dir, 0775);
138 } catch (IOExceptionInterface $e) {
139 //Throw error
140 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
141 }
142 }
143
144 //Get image width
145 $width = $image->getImageWidth();
146
147 //Get image height
148 $height = $image->getImageHeight();
149
150 //Create draw
151 $draw = new \ImagickDraw();
152
153 //Set stroke antialias
154 $draw->setStrokeAntialias(true);
155
156 //Set text antialias
157 $draw->setTextAntialias(true);
158
159 //Set font aliases
160 $fonts = [
161 'irishgrover' => $this->config['path']['public'].'/ttf/irishgrover.v10.ttf',
162 'droidsans' => $this->config['path']['public'].'/ttf/droidsans.regular.ttf',
163 'dejavusans' => $this->config['path']['public'].'/ttf/dejavusans.2.37.ttf',
164 'labelleaurore' => $this->config['path']['public'].'/ttf/labelleaurore.v10.ttf'
165 ];
166
167 //Set align aliases
168 $aligns = [
169 'left' => \Imagick::ALIGN_LEFT,
170 'center' => \Imagick::ALIGN_CENTER,
171 'right' => \Imagick::ALIGN_RIGHT
172 ];
173
174 //Set default font
175 $defaultFont = 'dejavusans';
176
177 //Set default align
178 $defaultAlign = 'center';
179
180 //Set default size
181 $defaultSize = 60;
182
183 //Set default stroke
184 $defaultStroke = '#00c3f9';
185
186 //Set default width
187 $defaultWidth = 15;
188
189 //Set default fill
190 $defaultFill = 'white';
191
192 //Init counter
193 $i = 1;
194
195 //Set text count
196 $count = count($texts);
197
198 //Draw each text stroke
199 foreach($texts as $text => $data) {
200 //Set font
201 $draw->setFont($fonts[$data['font']??$defaultFont]);
202
203 //Set font size
204 $draw->setFontSize($data['size']??$defaultSize);
205
206 //Set stroke width
207 $draw->setStrokeWidth($data['width']??$defaultWidth);
208
209 //Set text alignment
210 $draw->setTextAlignment($align = ($aligns[$data['align']??$defaultAlign]));
211
212 //Get font metrics
213 $metrics = $image->queryFontMetrics($draw, $text);
214
215 //Without y
216 if (empty($data['y'])) {
217 //Position verticaly each text evenly
218 $texts[$text]['y'] = $data['y'] = (($height + 100) / (count($texts) + 1) * $i) - 50;
219 }
220
221 //Without x
222 if (empty($data['x'])) {
223 if ($align == \Imagick::ALIGN_CENTER) {
224 $texts[$text]['x'] = $data['x'] = $width/2;
225 } elseif ($align == \Imagick::ALIGN_LEFT) {
226 $texts[$text]['x'] = $data['x'] = 50;
227 } elseif ($align == \Imagick::ALIGN_RIGHT) {
228 $texts[$text]['x'] = $data['x'] = $width - 50;
229 }
230 }
231
232 //Center verticaly
233 //XXX: add ascender part then center it back by half of textHeight
234 //TODO: maybe add a boundingbox ???
235 $texts[$text]['y'] = $data['y'] += $metrics['ascender'] - $metrics['textHeight']/2;
236
237 //Set stroke color
238 $draw->setStrokeColor(new \ImagickPixel($data['stroke']??$defaultStroke));
239
240 //Set fill color
241 $draw->setFillColor(new \ImagickPixel($data['stroke']??$defaultStroke));
242
243 //Add annotation
244 $draw->annotation($data['x'], $data['y'], $text);
245
246 //Increase counter
247 $i++;
248 }
249
250 //Create stroke object
251 $stroke = new \Imagick();
252
253 //Add new image
254 $stroke->newImage($width, $height, new \ImagickPixel('transparent'));
255
256 //Draw on image
257 $stroke->drawImage($draw);
258
259 //Blur image
260 //XXX: blur the stroke canvas only
261 $stroke->blurImage(5,3);
262
263 //Set opacity to 0.5
264 //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
265 $stroke->evaluateImage(\Imagick::EVALUATE_DIVIDE, 1.5, \Imagick::CHANNEL_ALPHA);
266
267 //Compose image
268 $image->compositeImage($stroke, \Imagick::COMPOSITE_OVER, 0, 0);
269
270 //Clear stroke
271 $stroke->clear();
272
273 //Destroy stroke
274 unset($stroke);
275
276 //Clear draw
277 $draw->clear();
278
279 //Set text antialias
280 $draw->setTextAntialias(true);
281
282 //Draw each text
283 foreach($texts as $text => $data) {
284 //Set font
285 $draw->setFont($fonts[$data['font']??$defaultFont]);
286
287 //Set font size
288 $draw->setFontSize($data['size']??$defaultSize);
289
290 //Set text alignment
291 $draw->setTextAlignment($aligns[$data['align']??$defaultAlign]);
292
293 //Set fill color
294 $draw->setFillColor(new \ImagickPixel($data['fill']??$defaultFill));
295
296 //Add annotation
297 $draw->annotation($data['x'], $data['y'], $text);
298
299 //With canonical text
300 if (!empty($data['canonical'])) {
301 //Prevent canonical to finish in alt
302 unset($texts[$text]);
303 }
304 }
305
306 //Draw on image
307 $image->drawImage($draw);
308
309 //Strip image exif data and properties
310 $image->stripImage();
311
312 //Set image format
313 $image->setImageFormat('jpeg');
314
315 //Save image
316 if (!$image->writeImage($dest)) {
317 //Throw error
318 throw new \Exception(sprintf('Unable to write image "%s"', $dest));
319 }
320
321 //Get dest stat
322 $stat = stat($dest);
323
324 //Return image data
325 return [
326 'og:image' => $package->getAbsoluteUrl('@RapsysAir/facebook/'.$stat['mtime'].$pathInfo.'.jpeg'),
327 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
328 'og:image:height' => $height,
329 'og:image:width' => $width
330 ];
331 }
332
333 //Return empty array without image
334 return [];
335 }
336
337
338 /**
339 * Renders a view
340 *
341 * {@inheritdoc}
342 */
343 protected function render(string $view, array $parameters = [], Response $response = null): Response {
344 //Get request stack
345 $stack = $this->container->get('request_stack');
346
347 //Get current request
348 $request = $stack->getCurrentRequest();
349
350 //Get current locale
351 $locale = $request->getLocale();
352
353 //Set locale
354 $parameters['locale'] = str_replace('_', '-', $locale);
355
356 //Get router
357 $router = $this->container->get('router');
358
359 //Get context path
360 $pathInfo = $router->getContext()->getPathInfo();
361
362 //Iterate on locales excluding current one
363 foreach($this->config['locales'] as $current) {
364 //Set titles
365 $titles = [];
366
367 //Iterate on other locales
368 foreach(array_diff($this->config['locales'], [$current]) as $other) {
369 $titles[$other] = $this->translator->trans($this->config['languages'][$current], [], null, $other);
370 }
371
372 //Retrieve route matching path
373 $route = $router->match($pathInfo);
374
375 //Get route name
376 $name = $route['_route'];
377
378 //Unset route name
379 unset($route['_route']);
380
381 //With current locale
382 if ($current == $locale) {
383 //Set locale locales context
384 $parameters['canonical'] = $router->generate($name, ['_locale' => $current]+$route, UrlGeneratorInterface::ABSOLUTE_URL);
385 } else {
386 //Set locale locales context
387 $parameters['alternates'][str_replace('_', '-', $current)] = [
388 'absolute' => $router->generate($name, ['_locale' => $current]+$route, UrlGeneratorInterface::ABSOLUTE_URL),
389 'relative' => $router->generate($name, ['_locale' => $current]+$route),
390 'title' => implode('/', $titles),
391 'translated' => $this->translator->trans($this->config['languages'][$current], [], null, $current)
392 ];
393 }
394
395 //Add shorter locale
396 if (empty($parameters['alternates'][$shortCurrent = substr($current, 0, 2)])) {
397 //Set locale locales context
398 $parameters['alternates'][$shortCurrent] = [
399 'absolute' => $router->generate($name, ['_locale' => $current]+$route, UrlGeneratorInterface::ABSOLUTE_URL),
400 'relative' => $router->generate($name, ['_locale' => $current]+$route),
401 'title' => implode('/', $titles),
402 'translated' => $this->translator->trans($this->config['languages'][$current], [], null, $current)
403 ];
404 }
405 }
406
407 //Create application form for role_guest
408 if ($this->isGranted('ROLE_GUEST')) {
409 //Without application form
410 if (empty($parameters['forms']['application'])) {
411 //Fetch doctrine
412 $doctrine = $this->getDoctrine();
413
414 //Create ApplicationType form
415 $application = $this->createForm('Rapsys\AirBundle\Form\ApplicationType', null, [
416 //Set the action
417 'action' => $this->generateUrl('rapsys_air_application_add'),
418 //Set the form attribute
419 'attr' => [ 'class' => 'col' ],
420 //Set admin
421 'admin' => $this->isGranted('ROLE_ADMIN'),
422 //Set default user to current
423 'user' => $this->getUser()->getId(),
424 //Set default slot to evening
425 //XXX: default to Evening (3)
426 'slot' => $doctrine->getRepository(Slot::class)->findOneById(3)
427 ]);
428
429 //Add form to context
430 $parameters['forms']['application'] = $application->createView();
431 }
432 //Create login form for anonymous
433 } elseif (!$this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
434 //Create LoginType form
435 $login = $this->createForm('Rapsys\UserBundle\Form\LoginType', null, [
436 //Set the action
437 'action' => $this->generateUrl('rapsys_user_login'),
438 //Disable password repeated
439 'password_repeated' => false,
440 //Set the form attribute
441 'attr' => [ 'class' => 'col' ]
442 ]);
443
444 //Add form to context
445 $parameters['forms']['login'] = $login->createView();
446
447 //Set field
448 $field = [
449 //With mail
450 'mail' => true,
451 //Without civility
452 'civility' => false,
453 //Without pseudonym
454 'pseudonym' => false,
455 //Without forename
456 'forename' => false,
457 //Without surname
458 'surname' => false,
459 //Without password
460 'password' => false,
461 //Without phone
462 'phone' => false
463 ];
464
465 //Get slugger
466 $slugger = $this->container->get('rapsys_pack.slugger_util');
467
468 //Create RegisterType form
469 $register = $this->createForm('Rapsys\AirBundle\Form\RegisterType', null, $field+[
470 //Set the action
471 'action' => $this->generateUrl(
472 'rapsys_user_register',
473 [
474 'mail' => $smail = $slugger->short(''),
475 'field' => $sfield = $slugger->serialize($field),
476 'hash' => $slugger->hash($smail.$sfield)
477 ]
478 ),
479 //Set the form attribute
480 'attr' => [ 'class' => 'col' ]
481 ]);
482
483 //Add form to context
484 $parameters['forms']['register'] = $register->createView();
485 }
486
487 //With page infos and without facebook texts
488 if (empty($parameters['facebook']['texts']) && !empty($parameters['site']['title']) && !empty($parameters['page']['title']) && !empty($parameters['canonical'])) {
489 //Set facebook image
490 $parameters['facebook']['texts'] = [
491 $parameters['site']['title'] => [
492 'font' => 'irishgrover',
493 'size' => 110
494 ],
495 $parameters['page']['title'] => [
496 'align' => 'left'
497 ],
498 $parameters['canonical'] => [
499 'align' => 'right',
500 'canonical' => true,
501 'font' => 'labelleaurore',
502 'size' => 50
503 ]
504 ];
505 }
506
507 //With canonical
508 if (!empty($parameters['canonical'])) {
509 //Set facebook url
510 $parameters['facebook']['metas']['og:url'] = $parameters['canonical'];
511 }
512
513 //With page title
514 if (!empty($parameters['page']['title'])) {
515 //Set facebook title
516 $parameters['facebook']['metas']['og:title'] = $parameters['page']['title'];
517 }
518
519 //With page description
520 if (!empty($parameters['page']['description'])) {
521 //Set facebook description
522 $parameters['facebook']['metas']['og:description'] = $parameters['page']['description'];
523 }
524
525 //With locale
526 if (!empty($locale)) {
527 //Set facebook locale
528 $parameters['facebook']['metas']['og:locale'] = $locale;
529
530 //With alternates
531 //XXX: locale change when fb_locale=xx_xx is provided is done in FacebookSubscriber
532 //XXX: see https://stackoverflow.com/questions/20827882/in-open-graph-markup-whats-the-use-of-oglocalealternate-without-the-locati
533 if (!empty($parameters['alternates'])) {
534 //Iterate on alternates
535 foreach($parameters['alternates'] as $lang => $alternate) {
536 if (strlen($lang) == 5) {
537 //Set facebook locale alternate
538 $parameters['facebook']['metas']['og:locale:alternate'] = str_replace('-', '_', $lang);
539 }
540 }
541 }
542 }
543
544 //Without facebook image defined and texts
545 if (empty($parameters['facebook']['metas']['og:image']) && !empty($parameters['facebook']['texts'])) {
546 //Get facebook image
547 $parameters['facebook']['metas'] += $this->getFacebookImage($pathInfo, $parameters['facebook']);
548 }
549
550 //Call parent method
551 return $this->baseRender($view, $parameters, $response);
552 }
553 }