From 1a4bd13fd71e305425e3a553e3bedf086c05b7e1 Mon Sep 17 00:00:00 2001 From: Andrey Shkodyak Date: Fri, 3 Aug 2012 18:55:35 +0300 Subject: [PATCH 1/2] Added multi language support to portfolio bundle --- Admin/ProjectAdmin.php | 30 ++- Controller/CategoryController.php | 2 +- Controller/ProjectController.php | 2 +- Entity/Project.php | 76 +++++- Entity/ProjectTranslation.php | 43 ++++ Form/TranslationLocaleType.php | 55 +++++ Form/TranslationsType.php | 228 ++++++++++++++++++ Resources/config/routing.yml | 13 +- Resources/config/services.xml | 15 ++ .../list_translations_field.html.twig | 9 + Resources/views/public/admin.css | 0 11 files changed, 450 insertions(+), 23 deletions(-) create mode 100644 Entity/ProjectTranslation.php create mode 100644 Form/TranslationLocaleType.php create mode 100644 Form/TranslationsType.php create mode 100644 Resources/views/ProjectAdmin/list_translations_field.html.twig create mode 100644 Resources/views/public/admin.css diff --git a/Admin/ProjectAdmin.php b/Admin/ProjectAdmin.php index 4d6ed64..e8d7ff2 100644 --- a/Admin/ProjectAdmin.php +++ b/Admin/ProjectAdmin.php @@ -25,15 +25,26 @@ public function __construct($code, $class, $baseControllerName) protected function configureFormFields(FormMapper $formMapper) { $formMapper - ->add('name') - ->add('slug') + ->with('General') + ->add('name') + ->add('slug') + ->add('description') + ->add('translations', 'project_translations', array( + 'by_reference' => false, + 'attr' => array( + 'class' => 'project-translations', + ), + 'locales' => array('uk', 'en') + )) + ->add('url') - ->add('description') - ->add('imageFile', 'file', array('required' => false, 'data_class' => 'Symfony\Component\HttpFoundation\File\File')) - ->add('date', 'date') - ->add('categories') - ->add('users') - ->add('onFrontPage', 'checkbox', array('required' => false)) + ->with('Options') + ->add('imageFile', 'file', array('required' => false, 'data_class' => 'Symfony\Component\HttpFoundation\File\File')) + ->add('date', 'date') + ->add('categories') + ->add('users') + ->add('onFrontPage', 'checkbox', array('required' => false)) + ->end(); ; } @@ -44,6 +55,9 @@ protected function configureListFields(ListMapper $listMapper) ->addIdentifier('slug') ->add('name') ->add('date') + ->add('translations', 'text', array( + 'template' => 'StfalconPortfolioBundle:ProjectAdmin:list_translations_field.html.twig' + )) ; } diff --git a/Controller/CategoryController.php b/Controller/CategoryController.php index 6fd5af0..962e138 100644 --- a/Controller/CategoryController.php +++ b/Controller/CategoryController.php @@ -24,7 +24,7 @@ class CategoryController extends Controller * * @return array * @Route( - * "/portfolio/{slug}/{page}", + * "{slug}/{page}", * name="portfolio_category_view", * requirements={"page" = "\d+"}, * defaults={"page" = "1"} diff --git a/Controller/ProjectController.php b/Controller/ProjectController.php index 012e7cf..e687911 100644 --- a/Controller/ProjectController.php +++ b/Controller/ProjectController.php @@ -22,7 +22,7 @@ class ProjectController extends Controller * @param string $projectSlug Slug of project * * @return array - * @Route("/portfolio/{categorySlug}/{projectSlug}", name="portfolio_project_view") + * @Route("/{categorySlug}/{projectSlug}", name="portfolio_project_view") * @Template() */ public function viewAction($categorySlug, $projectSlug) diff --git a/Entity/Project.php b/Entity/Project.php index 1783d98..7589b10 100644 --- a/Entity/Project.php +++ b/Entity/Project.php @@ -3,12 +3,14 @@ namespace Stfalcon\Bundle\PortfolioBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Translatable\Translatable; +use Imagine; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Validator\Constraints as Assert; use Vich\UploaderBundle\Mapping\Annotation as Vich; -use Gedmo\Mapping\Annotation as Gedmo; -use Imagine; /** * Project entity @@ -16,8 +18,9 @@ * @ORM\Table(name="portfolio_projects") * @ORM\Entity(repositoryClass="Stfalcon\Bundle\PortfolioBundle\Repository\ProjectRepository") * @Vich\Uploadable + * @Gedmo\TranslationEntity(class="Stfalcon\Bundle\PortfolioBundle\Entity\ProjectTranslation") */ -class Project +class Project implements Translatable { /** @@ -34,6 +37,7 @@ class Project * * @Assert\NotBlank() * @Assert\MinLength(3) + * @Gedmo\Translatable * @ORM\Column(name="name", type="string", length=255) */ private $name; @@ -52,6 +56,7 @@ class Project * * @Assert\NotBlank() * @Assert\MinLength(10) + * @Gedmo\Translatable * @ORM\Column(name="description", type="text") */ private $description; @@ -145,13 +150,35 @@ class Project private $users; /** - * Initialization properties for new project entity + * @var ArrayCollection * - * @return void + * @ORM\OneToMany(targetEntity="ProjectTranslation", mappedBy="object", cascade={"persist", "remove"}) + */ + protected $translations; + + /** + * @var string $locale + * + * Required for Translatable behaviour + * @Gedmo\Locale + */ + protected $locale; + + /** + * Initialization properties for new project entity */ public function __construct() { - $this->categories = new ArrayCollection(); + $this->categories = new ArrayCollection(); + $this->translations = new ArrayCollection(); + } + + /** + * @return string + */ + public function __toString() + { + return $this->getName(); } /** @@ -189,7 +216,7 @@ public function addCategory(Category $category) /** * Set categories collection to project * - * @param \Doctrine\Common\Collections\Collection $categories Categories collection + * @param Collection $categories Categories collection * * @return void */ @@ -486,4 +513,39 @@ public function getImageFile() { return $this->imageFile; } + + /** + * @return ArrayCollection + */ + public function getTranslations() + { + return $this->translations; + } + + /** + * @param ProjectTranslation $projectTranslation + */ + public function addTranslation(ProjectTranslation $projectTranslation) + { + if (!$this->translations->contains($projectTranslation)) { + $this->translations->add($projectTranslation); + $projectTranslation->setObject($this); + } + } + + /** + * @param ProjectTranslation $projectTranslation + */ + public function removeTranslation(ProjectTranslation $projectTranslation) + { + $this->translations->removeElement($projectTranslation); + } + + /** + * @param Collection $translations + */ + public function setTranslations(Collection $translations) + { + $this->translations = $translations; + } } \ No newline at end of file diff --git a/Entity/ProjectTranslation.php b/Entity/ProjectTranslation.php new file mode 100644 index 0000000..b4e6729 --- /dev/null +++ b/Entity/ProjectTranslation.php @@ -0,0 +1,43 @@ +setLocale($locale); + $this->setField($field); + $this->setContent($content); + } + + /** + * @ORM\ManyToOne(targetEntity="Project", inversedBy="translations") + * @ORM\JoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE") + */ + protected $object; + + function __toString() + { + return $this->getLocale(); + } +} + diff --git a/Form/TranslationLocaleType.php b/Form/TranslationLocaleType.php new file mode 100644 index 0000000..fb626fb --- /dev/null +++ b/Form/TranslationLocaleType.php @@ -0,0 +1,55 @@ + $type) { + $builder->add($field, $type, array( + 'label' => ucfirst($field), + 'required' => false, + 'attr' => array( + 'class' => 'span5' + ) + )); + } + } + + /** + * @param OptionsResolverInterface $resolver + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'error_bubbling' => true, + 'fields' => array(), + )); + } + + /** + * Get form name + * + * @return string + */ + public function getName() + { + return 'project_translation_locale'; + } +} diff --git a/Form/TranslationsType.php b/Form/TranslationsType.php new file mode 100644 index 0000000..fadb025 --- /dev/null +++ b/Form/TranslationsType.php @@ -0,0 +1,228 @@ +em = $em; + $this->annotationReader = $annotationReader; + $this->translatableListener = $translatableListener; + } + + /** + * Builds the form. + * + * This method is called for each type in the hierarchy starting form the + * top most type. Type extensions can further modify the form. + * + * @see FormTypeExtensionInterface::buildForm() + * + * @param FormBuilderInterface $builder The form builder + * @param array $options The options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + + $projectTranslationClass = $builder->getParent()->getDataClass(); + + $configuration = $this->translatableListener->getConfiguration($this->em, $projectTranslationClass); + + $projectClass = $configuration['useObjectClass']; + + $fields = array(); + foreach ($configuration['fields'] as $field) { + $annotations = $this->annotationReader->getPropertyAnnotations(new \ReflectionProperty($projectClass, $field)); + $mappingColumn = array_filter($annotations, function($item) + { + return $item instanceof \Doctrine\ORM\Mapping\Column; + }); + $mappingColumnCurrent = current($mappingColumn); + // Convert field type + switch ($mappingColumnCurrent->type) { + case 'string': + $fields[$field] = 'text'; + break; + case 'text': + $fields[$field] = 'textarea'; + break; + } + } + + // Build sub form for the each locale + foreach ($options['locales'] as $locale) { + $builder->add($locale, 'project_translation_locale', array( + 'fields' => $fields + )); + } + + //Add event listeners + $this->preSetEventHandler($builder); + + $this->bindEventHandler($builder, $configuration); + } + + /** + * @param FormView $view + * @param FormInterface $form + * @param array $options + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['locales'] = $options['locales']; + } + + /** + * @param OptionsResolverInterface $resolver + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'locales' => array() + )); + } + + /** + * @return string + */ + public function getName() + { + return 'project_translations'; + } + + + /** + * Bind event handler + * + * @param FormBuilderInterface $builder + * @param array $configuration + */ + private function bindEventHandler(FormBuilderInterface $builder, $configuration) + { + $builder->addEventListener(FormEvents::BIND, function(FormEvent $event) use ($builder, $configuration) + { + $form = $event->getForm(); + $data = $event->getData(); + + if (is_array($data)) { + $data = new ArrayCollection(); + + } else { + // Remove new elements with wrong format + foreach ($data as $key => $d) { + if (!is_numeric($key)) { + $data->removeElement($d); + } + } + } + + // Add/Update new elements with right format + $newData = new ArrayCollection(); + foreach ($form->getChildren() as $translationsLocaleForm) { + $locale = $translationsLocaleForm->getName(); + foreach ($translationsLocaleForm->getChildren() as $translation) { + $field = $translation->getName(); + $content = $translation->getData(); + + $existingTranslationEntity = $data->filter(function($entity) use ($locale, $field) + { + return ($entity->getLocale() === $locale && $entity->getField() === $field); + })->first(); + + if ($existingTranslationEntity) { + $existingTranslationEntity->setContent($content); + $newData->add($existingTranslationEntity); + } else { + $translationEntity = new ProjectTranslation(); + $translationEntity->setLocale($locale); + $translationEntity->setField($field); + $translationEntity->setContent($content); + $newData->add($translationEntity); + } + } + } + + $event->setData($newData); + }); + } + + /** + * Pre-set event handler + * + * @param FormBuilderInterface $builder + */ + private function preSetEventHandler(FormBuilderInterface $builder) + { + $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($builder) + { + $form = $event->getForm(); + $data = $event->getData(); + + if (is_null($data)) { + return; + } + + // Sort by locales and fields + $dataLocale = array(); + foreach ($data as $item) { + if (!isset($dataLocale[$item->getLocale()])) { + $dataLocale[$item->getLocale()] = new ArrayCollection(); + } + $dataLocale[$item->getLocale()][$item->getField()] = $item; + } + + foreach ($form->getChildren() as $translationFields) { + $locale = $translationFields->getName(); + if (isset($dataLocale[$locale])) { + foreach ($translationFields as $translationField) { + $field = $translationField->getName(); + if (isset($dataLocale[$locale][$field])) { + $translationField->setData($dataLocale[$locale][$field]->getContent()); + } + } + } + } + }); + } +} + diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index 5d9da99..2a91678 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -1,7 +1,8 @@ -_portfolio_category: - resource: "@StfalconPortfolioBundle/Controller/CategoryController.php" +_portfolio: + resource: "@StfalconPortfolioBundle/Controller" type: annotation - -_portfolio_project: - resource: "@StfalconPortfolioBundle/Controller/ProjectController.php" - type: annotation \ No newline at end of file + prefix: /{_locale}/portfolio + requirements: + _locale: ru|uk|en + defaults: + _locale: %locale% \ No newline at end of file diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 2242127..fffafd2 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -10,6 +10,9 @@ Stfalcon\Bundle\PortfolioBundle\Admin\ProjectAdmin Stfalcon\Bundle\PortfolioBundle\Entity\Project + + Stfalcon\Bundle\PortfolioBundle\Form\TranslationsType + Stfalcon\Bundle\PortfolioBundle\Form\TranslationLocaleType @@ -31,6 +34,18 @@ + + + + + + + + + + + + diff --git a/Resources/views/ProjectAdmin/list_translations_field.html.twig b/Resources/views/ProjectAdmin/list_translations_field.html.twig new file mode 100644 index 0000000..caa9ae7 --- /dev/null +++ b/Resources/views/ProjectAdmin/list_translations_field.html.twig @@ -0,0 +1,9 @@ +{% extends 'SonataAdminBundle:CRUD:base_list_field.html.twig' %} + +{%- block field %} + {% if value %} + {% for locale in value if locale.field == 'name' %} + {{ locale|upper }} + {% endfor %} + {% endif %} +{% endblock -%} diff --git a/Resources/views/public/admin.css b/Resources/views/public/admin.css new file mode 100644 index 0000000..e69de29 From c44c85db856b2478987ae48c6d3ee07fab507f75 Mon Sep 17 00:00:00 2001 From: Andrey Shkodyak Date: Fri, 3 Aug 2012 19:05:43 +0300 Subject: [PATCH 2/2] Fixed fetching of fallback translation --- Entity/Project.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Entity/Project.php b/Entity/Project.php index 7589b10..173a0c3 100644 --- a/Entity/Project.php +++ b/Entity/Project.php @@ -37,7 +37,7 @@ class Project implements Translatable * * @Assert\NotBlank() * @Assert\MinLength(3) - * @Gedmo\Translatable + * @Gedmo\Translatable(fallback=true) * @ORM\Column(name="name", type="string", length=255) */ private $name; @@ -56,7 +56,7 @@ class Project implements Translatable * * @Assert\NotBlank() * @Assert\MinLength(10) - * @Gedmo\Translatable + * @Gedmo\Translatable(fallback=true) * @ORM\Column(name="description", type="text") */ private $description;