+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys BlogBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Rapsys\BlogBundle\Controller;
+
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\Persistence\ManagerRegistry;
+use Psr\Log\LoggerInterface;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+use Symfony\Component\Asset\PackageInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Form\FormFactoryInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\RouterInterface;
+use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
+use Symfony\Contracts\Service\ServiceSubscriberInterface;
+use Twig\Environment;
+
+use Rapsys\BlogBundle\Entity\Dance;
+use Rapsys\BlogBundle\Entity\Location;
+use Rapsys\BlogBundle\Entity\Slot;
+use Rapsys\BlogBundle\Entity\User;
+use Rapsys\BlogBundle\RapsysBlogBundle;
+
+use Rapsys\PackBundle\Util\FacebookUtil;
+use Rapsys\PackBundle\Util\ImageUtil;
+use Rapsys\PackBundle\Util\SluggerUtil;
+
+/**
+ * Provides common features needed in controllers.
+ *
+ * {@inheritdoc}
+ */
+abstract class AbstractController extends BaseAbstractController implements ServiceSubscriberInterface {
+ ///AuthorizationCheckerInterface instance
+ protected AuthorizationCheckerInterface $checker;
+
+ ///Config array
+ protected array $config;
+
+ ///Context array
+ protected array $context;
+
+ ///AccessDecisionManagerInterface instance
+ protected AccessDecisionManagerInterface $decision;
+
+ ///ManagerRegistry instance
+ protected ManagerRegistry $doctrine;
+
+ ///FacebookUtil instance
+ protected FacebookUtil $facebook;
+
+ ///FormFactoryInterface instance
+ protected FormFactoryInterface $factory;
+
+ ///Image util instance
+ protected ImageUtil $image;
+
+ ///Limit integer
+ protected int $limit;
+
+ ///Locale string
+ protected string $locale;
+
+ ///MailerInterface instance
+ protected MailerInterface $mailer;
+
+ ///EntityManagerInterface instance
+ protected EntityManagerInterface $manager;
+
+ ///Modified DateTime
+ protected \DateTime $modified;
+
+ ///PackageInterface instance
+ protected PackageInterface $package;
+
+ ///Request instance
+ protected Request $request;
+
+ ///Route string
+ protected string $route;
+
+ ///Route params array
+ protected array $routeParams;
+
+ ///Router instance
+ protected RouterInterface $router;
+
+ ///Slugger util instance
+ protected SluggerUtil $slugger;
+
+ ///RequestStack instance
+ protected RequestStack $stack;
+
+ ///Translator instance
+ protected TranslatorInterface $translator;
+
+ ///Twig\Environment instance
+ protected Environment $twig;
+
+ /**
+ * Abstract constructor
+ *
+ * @param AuthorizationCheckerInterface $checker The container instance
+ * @param ContainerInterface $container The container instance
+ * @param AccessDecisionManagerInterface $decision The decision instance
+ * @param ManagerRegistry $doctrine The doctrine instance
+ * @param FacebookUtil $facebook The facebook instance
+ * @param FormFactoryInterface $factory The factory instance
+ * @param ImageUtil $image The image instance
+ * @param MailerInterface $mailer The mailer instance
+ * @param EntityManagerInterface $manager The manager instance
+ * @param PackageInterface $package The package instance
+ * @param RouterInterface $router The router instance
+ * @param SluggerUtil $slugger The slugger instance
+ * @param RequestStack $stack The stack instance
+ * @param TranslatorInterface $translator The translator instance
+ * @param Environment $twig The twig environment instance
+ * @param integer $limit The page limit
+ *
+ * @TODO move all that stuff to setSlugger('@slugger') setters with a calls: [ setSlugger: [ '@slugger' ] ] to unbload classes ???
+ * @TODO add a calls: [ ..., prepare: ['@???'] ] that do all the logic that can't be done in constructor because various things are not available
+ */
+ public function __construct(AuthorizationCheckerInterface $checker, ContainerInterface $container, AccessDecisionManagerInterface $decision, ManagerRegistry $doctrine, FacebookUtil $facebook, FormFactoryInterface $factory, ImageUtil $image, MailerInterface $mailer, EntityManagerInterface $manager, PackageInterface $package, RouterInterface $router, SluggerUtil $slugger, RequestStack $stack, TranslatorInterface $translator, Environment $twig, int $limit = 5) {
+ //Set checker
+ $this->checker = $checker;
+
+ //Retrieve config
+ $this->config = $container->getParameter(RapsysBlogBundle::getAlias());
+
+ //Set the container
+ $this->container = $container;
+
+ //Set decision
+ $this->decision = $decision;
+
+ //Set doctrine
+ $this->doctrine = $doctrine;
+
+ //Set facebook
+ $this->facebook = $facebook;
+
+ //Set factory
+ $this->factory = $factory;
+
+ //Set image
+ $this->image = $image;
+
+ //Set limit
+ $this->limit = $limit;
+
+ //Set mailer
+ $this->mailer = $mailer;
+
+ //Set manager
+ $this->manager = $manager;
+
+ //Set package
+ $this->package = $package;
+
+ //Set router
+ $this->router = $router;
+
+ //Set slugger
+ $this->slugger = $slugger;
+
+ //Set stack
+ $this->stack = $stack;
+
+ //Set translator
+ $this->translator = $translator;
+
+ //Set twig
+ $this->twig = $twig;
+
+ //Get main request
+ $this->request = $this->stack->getMainRequest();
+
+ //Get current locale
+ $this->locale = $this->request->getLocale();
+
+ //Set canonical
+ $canonical = null;
+
+ //Set alternates
+ $alternates = [];
+
+ //Set route
+ //TODO: default to not found route ???
+ //TODO: when url not found, this attribute is not defined, how do we handle it ???
+ //XXX: do we generate a dummy default route by default ???
+ $this->route = $this->request->attributes->get('_route');
+
+ //Set route params
+ $this->routeParams = $this->request->attributes->get('_route_params');
+
+ //With route and routeParams
+ if ($this->route !== null && $this->routeParams !== null) {
+ //Set canonical
+ $canonical = $this->router->generate($this->route, $this->routeParams, UrlGeneratorInterface::ABSOLUTE_URL);
+
+ //Set alternates
+ $alternates = [];
+ }
+
+ //Set the context
+ $this->context = [
+ //TODO: review the structure
+ #'title' => $this->translator->trans($this->config['title']),
+ #'og:site_name' => $this->translator->trans($this->config['title']),
+ #'site' => [
+ # 'donate' => $this->config['donate'],
+ # 'title' => $title = $this->translator->trans($this->config['site']['title']),
+ #],
+ 'head' => [
+ 'alternates' => $alternates,
+ 'canonical' => $canonical,
+ 'icon' => $this->config['icon'],
+ 'keywords' => null,
+ 'locale' => str_replace('_', '-', $this->locale),
+ 'logo' => [
+ 'png' => $this->config['logo']['png'],
+ 'svg' => $this->config['logo']['svg'],
+ 'alt' => $this->translator->trans($this->config['logo']['alt'])
+ ],
+ 'root' => $this->config['root'],
+ 'title' => null,
+ 'facebook' => [
+ 'og:type' => 'article',
+ 'og:site_name' => $this->translator->trans($this->config['title']),
+ 'og:url' => $canonical,
+ //TODO: review this value
+ 'fb:app_id' => $this->config['facebook']['apps']
+ ],
+ 'fbimage' => [
+ 'texts' => [
+ $this->translator->trans($this->config['title']) => [
+ 'font' => 'irishgrover',
+ 'size' => 110
+ ]
+ ]
+ ]
+ ],
+ 'contact' => [
+ 'name' => $this->translator->trans($this->config['contact']['name']),
+ 'mail' => $this->config['contact']['mail']
+ ],
+ 'copy' => [
+ 'by' => $this->translator->trans($this->config['copy']['by']),
+ 'link' => $this->config['copy']['link'],
+ 'long' => $this->translator->trans($this->config['copy']['long']),
+ 'short' => $this->translator->trans($this->config['copy']['short']),
+ 'title' => $this->config['copy']['title']
+ ],
+ 'forms' => [],
+ 'title' => null,
+ 'description' => null,
+ 'section' => null,
+ 'site' => [
+ 'icon' => $this->config['icon'],
+ 'logo' => [
+ 'png' => $this->config['logo']['png'],
+ 'svg' => $this->config['logo']['svg'],
+ 'alt' => $this->translator->trans($this->config['logo']['alt'])
+ ],
+ 'path' => $this->config['path'],
+ 'root' => $this->config['root'],
+ 'title' => $this->translator->trans($this->config['title'])
+ ]
+ ];
+ }
+
+ /**
+ * Renders a view
+ *
+ * {@inheritdoc}
+ */
+ protected function render(string $view, array $parameters = [], Response $response = null): Response {
+ //Create response when null
+ $response ??= new Response();
+
+ //Without alternates
+ if (empty($parameters['head']['alternates'])) {
+ //Iterate on locales excluding current one
+ foreach($this->config['locales'] as $locale) {
+ //Set routeParams
+ $routeParams = $this->routeParams;
+
+ //With current locale
+ if ($locale !== $this->locale) {
+ //Set titles
+ $titles = [];
+
+ //Set route params locale
+ $routeParams['_locale'] = $locale;
+
+ //Unset slug
+ //XXX: slug is in current locale, better use a simple redirect for invalid slug than implement a hard to get translation here
+ unset($routeParams['slug']);
+
+ //Iterate on other locales
+ foreach(array_diff($this->config['locales'], [$locale]) as $other) {
+ //Set other locale title
+ $titles[$other] = $this->translator->trans($this->config['languages'][$locale], [], null, $other);
+ }
+
+ //Set locale locales context
+ $parameters['head']['alternates'][str_replace('_', '-', $locale)] = [
+ 'absolute' => $this->router->generate($this->route, $routeParams, UrlGeneratorInterface::ABSOLUTE_URL),
+ 'relative' => $this->router->generate($this->route, $routeParams),
+ 'title' => implode('/', $titles),
+ 'translated' => $this->translator->trans($this->config['languages'][$locale], [], null, $locale)
+ ];
+
+ //Add shorter locale
+ if (empty($parameters['head']['alternates'][$shortCurrent = substr($locale, 0, 2)])) {
+ //Set locale locales context
+ $parameters['head']['alternates'][$shortCurrent] = $parameters['head']['alternates'][str_replace('_', '-', $locale)];
+ }
+ //Add shorter locale
+ } elseif (empty($parameters['head']['alternates'][$shortCurrent = substr($locale, 0, 2)])) {
+ //Set titles
+ $titles = [];
+
+ //Set route params locale
+ $routeParams['_locale'] = $locale;
+
+ //Iterate on other locales
+ foreach(array_diff($this->config['locales'], [$locale]) as $other) {
+ //Set other locale title
+ $titles[$other] = $this->translator->trans($this->config['languages'][$locale], [], null, $other);
+ }
+
+ //Set locale locales context
+ $parameters['head']['alternates'][$shortCurrent] = [
+ 'absolute' => $this->router->generate($this->route, $routeParams, UrlGeneratorInterface::ABSOLUTE_URL),
+ 'relative' => $this->router->generate($this->route, $routeParams),
+ 'title' => implode('/', $titles),
+ 'translated' => $this->translator->trans($this->config['languages'][$locale], [], null, $locale)
+ ];
+ }
+ }
+ }
+
+ //With empty head title and section
+ if (empty($parameters['head']['title']) && !empty($parameters['section'])) {
+ //Set head title
+ $parameters['head']['title'] = implode(' - ', [$parameters['title'], $parameters['section'], $this->translator->trans($this->config['title'])]);
+ //With empty head title
+ } elseif (empty($parameters['head']['title'])) {
+ //Set head title
+ $parameters['head']['title'] = implode(' - ', [$parameters['title'], $this->translator->trans($this->config['title'])]);
+ }
+
+ //With empty head description and description
+ if (empty($parameters['head']['description']) && !empty($parameters['description'])) {
+ //Set head description
+ $parameters['head']['description'] = $parameters['description'];
+ }
+
+ //With empty facebook title and title
+ if (empty($parameters['head']['facebook']['og:title']) && !empty($parameters['title'])) {
+ //Set facebook title
+ $parameters['head']['facebook']['og:title'] = $parameters['title'];
+ }
+
+ //With empty facebook description and description
+ if (empty($parameters['head']['facebook']['og:description']) && !empty($parameters['description'])) {
+ //Set facebook description
+ $parameters['head']['facebook']['og:description'] = $parameters['description'];
+ }
+
+ //With locale
+ if (!empty($this->locale)) {
+ //Set facebook locale
+ $parameters['head']['facebook']['og:locale'] = $this->locale;
+
+ //With alternates
+ //XXX: locale change when fb_locale=xx_xx is provided is done in FacebookSubscriber
+ //XXX: see https://stackoverflow.com/questions/20827882/in-open-graph-markup-whats-the-use-of-oglocalealternate-without-the-locati
+ if (!empty($parameters['head']['alternates'])) {
+ //Iterate on alternates
+ foreach($parameters['head']['alternates'] as $lang => $alternate) {
+ if (strlen($lang) == 5) {
+ //Set facebook locale alternate
+ $parameters['head']['facebook']['og:locale:alternate'] = str_replace('-', '_', $lang);
+ }
+ }
+ }
+ }
+
+ //Without facebook image defined and texts
+ if (empty($parameters['head']['facebook']['og:image']) && !empty($this->request) && !empty($parameters['head']['fbimage']['texts']) && !empty($this->modified)) {
+ //Get facebook image
+ $parameters['head']['facebook'] += $this->facebook->getImage($this->request->getPathInfo(), $parameters['head']['fbimage']['texts'], $this->modified->getTimestamp());
+ }
+
+ //Call twig render method
+ $content = $this->twig->render($view, $parameters);
+
+ //Invalidate OK response on invalid form
+ if (200 === $response->getStatusCode()) {
+ foreach ($parameters as $v) {
+ if ($v instanceof FormInterface && $v->isSubmitted() && !$v->isValid()) {
+ $response->setStatusCode(422);
+ break;
+ }
+ }
+ }
+
+ //Store content in response
+ $response->setContent($content);
+
+ //Return response
+ return $response;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @see vendor/symfony/framework-bundle/Controller/AbstractController.php
+ */
+ public static function getSubscribedServices(): array {
+ //Return subscribed services
+ return [
+ 'security.authorization_checker' => AuthorizationCheckerInterface::class,
+ 'service_container' => ContainerInterface::class,
+ 'rapsys_user.access_decision_manager' => AccessDecisionManagerInterface::class,
+ 'doctrine' => ManagerRegistry::class,
+ 'rapsys_pack.facebook_util' => FacebookUtil::class,
+ 'form.factory' => FormFactoryInterface::class,
+ 'rapsys_pack.image_util' => ImageUtil::class,
+ 'mailer.mailer' => MailerInterface::class,
+ 'doctrine.orm.default_entity_manager' => EntityManagerInterface::class,
+ 'rapsys_pack.path_package' => PackageInterface::class,
+ 'router' => RouterInterface::class,
+ 'rapsys_pack.slugger_util' => SluggerUtil::class,
+ 'request_stack' => RequestStack::class,
+ 'translator' => TranslatorInterface::class,
+ 'twig' => Environment::class,
+ ];
+ }
+}