vendor/fp/jsformvalidator-bundle/src/Factory/JsFormValidatorFactory.php line 213

Open in your IDE?
  1. <?php
  2. namespace Fp\JsFormValidatorBundle\Factory;
  3. use Fp\JsFormValidatorBundle\Exception\UndefinedFormException;
  4. use Fp\JsFormValidatorBundle\Form\Constraint\UniqueEntity;
  5. use Fp\JsFormValidatorBundle\Model\JsConfig;
  6. use Fp\JsFormValidatorBundle\Model\JsFormElement;
  7. use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
  8. use Symfony\Component\Form\DataTransformerInterface;
  9. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  10. use Symfony\Component\Form\Extension\Core\Type\HiddenType;
  11. use Symfony\Component\Form\Form;
  12. use Symfony\Component\Form\FormInterface;
  13. use Symfony\Component\Translation\TranslatorInterface;
  14. use Symfony\Component\Validator\Constraint;
  15. use Symfony\Component\Validator\Mapping\ClassMetadata;
  16. use Symfony\Component\Validator\Mapping\GetterMetadata;
  17. use Symfony\Component\Validator\Mapping\PropertyMetadata;
  18. use Symfony\Component\Validator\Validator\ValidatorInterface;
  19. /**
  20.  * This factory uses to parse a form to a tree of JsFormElement's
  21.  *
  22.  * Class JsFormValidatorFactory
  23.  *
  24.  * @package Fp\JsFormValidatorBundle\Factory
  25.  */
  26. class JsFormValidatorFactory
  27. {
  28.     /**
  29.      * @var ValidatorInterface
  30.      */
  31.     protected $validator;
  32.     /**
  33.      * @var TranslatorInterface
  34.      */
  35.     protected $translator;
  36.     /**
  37.      * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
  38.      */
  39.     protected $router;
  40.     /**
  41.      * @var array
  42.      */
  43.     protected $config = array();
  44.     /**
  45.      * @var Form[]
  46.      */
  47.     protected $queue = array();
  48.     /**
  49.      * @var Form
  50.      */
  51.     protected $currentElement null;
  52.     /**
  53.      * @var string
  54.      */
  55.     protected $transDomain;
  56.     /**
  57.      * @param ValidatorInterface    $validator
  58.      * @param TranslatorInterface   $translator
  59.      * @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $router
  60.      * @param array                 $config
  61.      * @param string                $domain
  62.      */
  63.     public function __construct(
  64.         ValidatorInterface $validator,
  65.         TranslatorInterface $translator,
  66.         $router,
  67.         $config,
  68.         $domain
  69.     ) {
  70.         $this->validator   $validator;
  71.         $this->translator  $translator;
  72.         $this->router      $router;
  73.         $this->config      $config;
  74.         $this->transDomain $domain;
  75.     }
  76.     /**
  77.      * Gets metadata from system using the entity class name
  78.      *
  79.      * @param string $className
  80.      *
  81.      * @return ClassMetadata
  82.      * @codeCoverageIgnore
  83.      */
  84.     protected function getMetadataFor($className)
  85.     {
  86.         return $this->validator->getMetadataFor($className);
  87.     }
  88.     /**
  89.      * Translate a single message
  90.      *
  91.      * @param string $message
  92.      *
  93.      * @return string
  94.      * @codeCoverageIgnore
  95.      */
  96.     protected function translateMessage($message, array $parameters = array())
  97.     {
  98.         return $this->translator->trans($message$parameters$this->transDomain);
  99.     }
  100.     /**
  101.      * Generate an URL from the route
  102.      *
  103.      * @param string $route
  104.      *
  105.      * @return string
  106.      * @codeCoverageIgnore
  107.      */
  108.     protected function generateUrl($route)
  109.     {
  110.         return $this->router->generate($route);
  111.     }
  112.     /**
  113.      * Get Config
  114.      *
  115.      * @param null|string $name
  116.      *
  117.      * @return mixed
  118.      */
  119.     public function getConfig($name null)
  120.     {
  121.         if ($name) {
  122.             return isset($this->config[$name]) ? $this->config[$name] : null;
  123.         } else {
  124.             return $this->config;
  125.         }
  126.     }
  127.     public function createJsConfigModel()
  128.     {
  129.         $result = array();
  130.         if (!empty($this->config['routing'])) {
  131.             foreach ($this->config['routing'] as $param => $value) {
  132.                 try {
  133.                     $result['routing'][$param] = $this->generateUrl($value);
  134.                 } catch (\Exception $e) {
  135.                     $result['routing'][$param] = null;
  136.                 }
  137.             }
  138.         }
  139.         $model          = new JsConfig;
  140.         $model->routing $result['routing'];
  141.         return $model;
  142.     }
  143.     /**
  144.      * Returns the current queue
  145.      *
  146.      * @return \Symfony\Component\Form\Form[]
  147.      */
  148.     public function getQueue()
  149.     {
  150.         return $this->queue;
  151.     }
  152.     /**
  153.      * Add a new form to processing queue
  154.      *
  155.      * @param \Symfony\Component\Form\Form $form
  156.      *
  157.      * @return array
  158.      */
  159.     public function addToQueue(Form $form)
  160.     {
  161.         $this->queue[$form->getName()] = $form;
  162.     }
  163.     /**
  164.      * Check if form is already in queue
  165.      *
  166.      * @param Form $form
  167.      *
  168.      * @return bool
  169.      */
  170.     public function inQueue(Form $form)
  171.     {
  172.         return isset($this->queue[$form->getName()]);
  173.     }
  174.     /**
  175.      * Removes from the queue elements which are not parent forms and should not be processes
  176.      *
  177.      * @return $this
  178.      */
  179.     public function siftQueue()
  180.     {
  181.         foreach ($this->queue as $name => $form) {
  182.             $blockName $form->getConfig()->getOption('block_name');
  183.             if ('_token' == $name || 'entry' == $blockName || $form->getParent()) {
  184.                 unset($this->queue[$name]);
  185.             }
  186.         }
  187.         return $this;
  188.     }
  189.     /**
  190.      * @return JsFormElement[]
  191.      */
  192.     public function processQueue()
  193.     {
  194.         $result = array();
  195.         foreach ($this->queue as $form) {
  196.             if (null !== ($model $this->createJsModel($form))) {
  197.                 $result[] = $model;
  198.             }
  199.         };
  200.         $this->queue = array();
  201.         return $result;
  202.     }
  203.     /**
  204.      * The main function that creates nested model
  205.      *
  206.      * @param Form $form
  207.      *
  208.      * @return null|JsFormElement
  209.      */
  210.     public function createJsModel(Form $form)
  211.     {
  212.         $this->currentElement $form;
  213.         $conf $form->getConfig();
  214.         // If field is disabled or has no any validations
  215.         if (false === $conf->getOption('js_validation')) {
  216.             return null;
  217.         }
  218.         $model                 = new JsFormElement;
  219.         $model->id             $this->getElementId($form);
  220.         $model->name           $form->getName();
  221.         $model->type           get_class($conf->getType()->getInnerType());
  222.         $model->invalidMessage $this->translateMessage(
  223.             $conf->getOption('invalid_message'),
  224.             $conf->getOption('invalid_message_parameters')
  225.         );
  226.         $model->transformers   $this->normalizeViewTransformers(
  227.             $form,
  228.             $this->parseTransformers($conf->getViewTransformers())
  229.         );
  230.         $model->bubbling       $conf->getOption('error_bubbling');
  231.         $model->data           $this->getValidationData($form);
  232.         $model->children       $this->processChildren($form);
  233.         $prototype $form->getConfig()->getAttribute('prototype');
  234.         if ($prototype) {
  235.             $model->prototype $this->createJsModel($prototype);
  236.         }
  237.         // Return self id to add it as child to the parent model
  238.         return $model;
  239.     }
  240.     /**
  241.      * Create the JsFormElement for all the children of specified element
  242.      *
  243.      * @param null|Form $form
  244.      *
  245.      * @return array
  246.      */
  247.     protected function processChildren($form)
  248.     {
  249.         $result = array();
  250.         // If this field has children - process them
  251.         foreach ($form as $name => $child) {
  252.             if ($this->isProcessableElement($child)) {
  253.                 $childModel $this->createJsModel($child);
  254.                 if (null !== $childModel) {
  255.                     $result[$name] = $childModel;
  256.                 }
  257.             }
  258.         }
  259.         return $result;
  260.     }
  261.     /**
  262.      * Generate an Id for the element by merging the current element name
  263.      * with all the parents names
  264.      *
  265.      * @param Form $form
  266.      *
  267.      * @return string
  268.      */
  269.     protected function getElementId(Form $form)
  270.     {
  271.         /** @var Form $parent */
  272.         $parent $form->getParent();
  273.         if (null !== $parent) {
  274.             return $this->getElementId($parent) . '_' $form->getName();
  275.         } else {
  276.             return $form->getName();
  277.         }
  278.     }
  279.     /**
  280.      * @param Form $form
  281.      *
  282.      * @return array
  283.      */
  284.     protected function getValidationData(Form $form)
  285.     {
  286.         // If parent has metadata
  287.         $parent $form->getParent();
  288.         if ($parent && null !== $parent->getConfig()->getDataClass()) {
  289.             $classMetadata $metadata $this->getMetadataFor($parent->getConfig()->getDataClass());
  290.             if ($classMetadata->hasPropertyMetadata($form->getName())) {
  291.                 $metadata $classMetadata->getPropertyMetadata($form->getName());
  292.                 /** @var PropertyMetadata $item */
  293.                 foreach ($metadata as $item) {
  294.                     $this->composeValidationData(
  295.                         $parentData,
  296.                         $item->getConstraints(),
  297.                         $getters = !empty($item->getters) ? (array)$item->getters : array()
  298.                     );
  299.                 }
  300.             }
  301.         }
  302.         // If has own metadata
  303.         if (null !== $form->getConfig()->getDataClass()) {
  304.             $metadata $this->getMetadataFor($form->getConfig()->getDataClass());
  305.             $this->composeValidationData(
  306.                 $ownData,
  307.                 $metadata->getConstraints(),
  308.                 $getters = !empty($metadata->getters) ? (array)$metadata->getters : array()
  309.             );
  310.         }
  311.         // If has constraints in a form element
  312.         $this->composeValidationData(
  313.             $formData,
  314.             (array)$form->getConfig()->getOption('constraints'),
  315.             array()
  316.         );
  317.         $result = array();
  318.         $groups $this->getValidationGroups($form);
  319.         if (!empty($parentData)) {
  320.             $parentData['groups'] = $this->getValidationGroups($parent);
  321.             $result['parent']     = $parentData;
  322.         }
  323.         if (!empty($ownData)) {
  324.             $ownData['groups'] = $groups;
  325.             $result['entity']  = $ownData;
  326.         }
  327.         if (!empty($formData)) {
  328.             $formData['groups'] = $groups;
  329.             $result['form']     = $formData;
  330.         }
  331.         return $result;
  332.     }
  333.     protected function mergeDataRecursive(array $array1, array $array2)
  334.     {
  335.         foreach ($array2 as $key => $value) {
  336.             if (empty($array1[$key])) {
  337.                 $array1[$key] = $value;
  338.             } elseif (is_array($value)) {
  339.                 if ((array_keys($value) !== range(0count($value) - 1))) {
  340.                     $array1[$key] = $this->mergeDataRecursive($array1[$key], $value);
  341.                 } else {
  342.                     $array1[$key] = array_merge($array1[$key], $value);
  343.                 }
  344.             } else {
  345.                 $array1[$key] = $value;
  346.             }
  347.         }
  348.         return $array1;
  349.     }
  350.     /**
  351.      * @param array            $container
  352.      * @param Constraint[]     $constraints
  353.      * @param GetterMetadata[] $getters
  354.      *
  355.      * @return void
  356.      */
  357.     public function composeValidationData(&$container$constraints$getters)
  358.     {
  359.         if (null == $container) {
  360.             $container = array();
  361.         }
  362.         if ($getters) {
  363.             if (!isset($container['getters'])) {
  364.                 $container['getters'] = array();
  365.             }
  366.             $container['getters'] = array_merge($container['getters'], $this->parseGetters($getters));
  367.         }
  368.         if ($constraints) {
  369.             if (!isset($container['constraints'])) {
  370.                 $container['constraints'] = array();
  371.             }
  372.             $container['constraints'] = array_merge($container['constraints'], $this->parseConstraints($constraints));
  373.         }
  374.     }
  375.     /**
  376.      * Get validation groups for the specified form
  377.      *
  378.      * @param Form|FormInterface $form
  379.      *
  380.      * @return array|string
  381.      */
  382.     protected function getValidationGroups(Form $form)
  383.     {
  384.         $result = array('Default');
  385.         $groups $form->getConfig()->getOption('validation_groups');
  386.         if (empty($groups)) {
  387.             // Try to get groups from a parent
  388.             if ($form->getParent()) {
  389.                 $result $this->getValidationGroups($form->getParent());
  390.             }
  391.         } elseif (is_array($groups)) {
  392.             // If groups is an array - return groups as is
  393.             $result $groups;
  394.         } elseif ($groups instanceof \Closure) {
  395.             // If groups is a Closure - return the form class name to look for javascript
  396.             $result $this->getElementId($form);
  397.         }
  398.         return $result;
  399.     }
  400.     /**
  401.      * Not all elements should be processed by thy factory (e.g. buttons, hidden inputs etc)
  402.      *
  403.      * @param mixed $element
  404.      *
  405.      * @return bool
  406.      */
  407.     protected function isProcessableElement($element)
  408.     {
  409.         return ($element instanceof Form) && (!is_a($element->getConfig()->getType(), HiddenType::class, true));
  410.     }
  411.     /**
  412.      * Gets view transformers from the given form.
  413.      * Merges in an extra Choice(s)ToBooleanArrayTransformer transformer in case of expanded choice.
  414.      *
  415.      * @param FormInterface $form
  416.      * @param array $viewTransformers
  417.      *
  418.      * @return array
  419.      */
  420.     protected function normalizeViewTransformers(FormInterface $form, array $viewTransformers)
  421.     {
  422.         $config $form->getConfig();
  423.         // Choice(s)ToBooleanArrayTransformer was deprecated in SF2.7 in favor of CheckboxListMapper and RadioListMapper
  424.         if ($config->getType()->getInnerType() instanceof ChoiceType && $config->getOption('expanded')) {
  425.             $namespace 'Symfony\Component\Form\Extension\Core\DataTransformer\\';
  426.             $transformer $config->getOption('multiple')
  427.                 ? array('name' => $namespace 'ChoicesToBooleanArrayTransformer')
  428.                 : array('name' => $namespace 'ChoiceToBooleanArrayTransformer');
  429.             $transformer['choiceList'] = array_values($config->getOption('choices'));
  430.             array_unshift($viewTransformers$transformer);
  431.         }
  432.         return $viewTransformers;
  433.     }
  434.     /**
  435.      * Convert transformers objects to data arrays
  436.      *
  437.      * @param array $transformers
  438.      *
  439.      * @return array
  440.      */
  441.     protected function parseTransformers(array $transformers)
  442.     {
  443.         $result = array();
  444.         foreach ($transformers as $trans) {
  445.             $item = array();
  446.             $reflect    = new \ReflectionClass($trans);
  447.             $properties $reflect->getProperties();
  448.             foreach ($properties as $prop) {
  449.                 $item[$prop->getName()] = $this->getTransformerParam($trans$prop->getName());
  450.             }
  451.             $item['name'] = get_class($trans);
  452.             $result[] = $item;
  453.         }
  454.         return $result;
  455.     }
  456.     /**
  457.      * Get the specified non-public transformer property
  458.      *
  459.      * @param DataTransformerInterface $transformer
  460.      * @param string                   $paramName
  461.      *
  462.      * @return mixed
  463.      */
  464.     protected function getTransformerParam(DataTransformerInterface $transformer$paramName)
  465.     {
  466.         $reflection = new \ReflectionProperty($transformer$paramName);
  467.         $reflection->setAccessible(true);
  468.         $value  $reflection->getValue($transformer);
  469.         $result null;
  470.         if ('transformers' === $paramName && is_array($value)) {
  471.             $result $this->parseTransformers($value);
  472.         } elseif (is_scalar($value) || is_array($value)) {
  473.             $result $value;
  474.         } elseif ($value instanceof ChoiceListInterface) {
  475.             $result array_values($value->getChoices());
  476.         }
  477.         return $result;
  478.     }
  479.     /**
  480.      * Converts list of the GetterMetadata objects to a data array
  481.      *
  482.      * @param GetterMetadata[] $getters
  483.      *
  484.      * @return array
  485.      */
  486.     protected function parseGetters(array $getters)
  487.     {
  488.         $result = array();
  489.         foreach ($getters as $getter) {
  490.             $result[$getter->getName()] = $this->parseConstraints((array)$getter->getConstraints());
  491.         }
  492.         return $result;
  493.     }
  494.     /**
  495.      * Converts list of constraints objects to a data array
  496.      *
  497.      * @param array $constraints
  498.      *
  499.      * @return array
  500.      */
  501.     protected function parseConstraints(array $constraints)
  502.     {
  503.         $result = array();
  504.         foreach ($constraints as $item) {
  505.             // Translate messages if need and add to result
  506.             foreach ($item as $propName => $propValue) {
  507.                 if (false !== strpos(strtolower($propName), 'message')) {
  508.                     $item->{$propName} = $this->translateMessage($propValue);
  509.                 }
  510.             }
  511.             if ($item instanceof \Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity) {
  512.                 $item = new UniqueEntity($item$this->currentElement->getConfig()->getDataClass());
  513.             }
  514.             $result[get_class($item)][] = $item;
  515.         }
  516.         return $result;
  517.     }
  518.     public function getJsConfigString()
  519.     {
  520.         return '<script type="text/javascript">FpJsFormValidator.config = ' $this->createJsConfigModel() . ';</script>';
  521.     }
  522.     /**
  523.      * @param string $formName
  524.      * @param bool   $onLoad
  525.      *
  526.      * @throws \Fp\JsFormValidatorBundle\Exception\UndefinedFormException
  527.      * @return string
  528.      */
  529.     public function getJsValidatorString($formName null$onLoad true)
  530.     {
  531.         $onLoad $onLoad 'true' 'false';
  532.         $this->siftQueue();
  533.         $models = array();
  534.         // Process just the specified form
  535.         if ($formName) {
  536.             if (!isset($this->queue[$formName])) {
  537.                 $list implode(', 'array_keys($this->queue));
  538.                 throw new UndefinedFormException("Form '$formName' was not found. Existing forms: $list");
  539.             }
  540.             $models[] = $this->createJsModel($this->queue[$formName]);
  541.             unset($this->queue[$formName]);
  542.         } else { // Or process whole queue
  543.             $models $this->processQueue();
  544.         }
  545.         // If there are no forms to validate
  546.         if (!array_filter($models)) {
  547.             return '';
  548.         }
  549.         $result = array();
  550.         foreach ($models as $model) {
  551.             $result[] = "FpJsFormValidator.addModel({$model}{$onLoad});";
  552.         }
  553.         return implode("\n"$result);
  554.     }
  555. }