Laravel.io
<?php

namespace OpenObjects\Bundle\CoreBundle\Service;

use OpenObjects\Bundle\StyleGuideBundle\Form\FieldType\FieldsetComponentType;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormError;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Validator\ConstraintViolation;

/**
 * Class FormErrorsSerializer
 * @package OpenObjects\Bundle\CoreBundle\Service
 */
class FormErrorsSerializer
{
    /**
     * @var bool
     */
    private $breakErrorLoop = false;

    /**
     * @var array
     */
    private $originalFormErrors = [];

    /**
     * @param Form $form
     * @param bool $flatArray
     * @param bool $addFormName
     * @param string $glueKeys
     *
     * @return array
     */
    public function serializeFormErrors(Form $form, $flatArray = false, $addFormName = false, $glueKeys = '_')
    {
        $errors = [];
        $errors['global'] = [];
        $errors['fields'] = [];

        foreach ($form->getErrors() as $error) {
            $errors['global'][] = $error->getMessage();
        }

        $errors['fields'] = $this->serialize($form);

        if ($flatArray) {
            $errors['fields'] = $this->arrayFlatten(
                $errors['fields'],
                $glueKeys,
                (($addFormName) ? $form->getName() : '')
            );
        }

        return $errors;
    }


    /**
     * @param Form $form
     *
     * @return array
     */
    private function serialize(Form $form) : array
    {
        $localErrors = [];

        if ($form->getErrors()->count() > 0) {
            foreach ($form->getErrors() as $error) {
                $localErrors = $this->buildErrorArray($error);
            }
        } else {
            foreach ($form->getIterator() as $key => $child) {
                foreach ($child->getErrors() as $error) {
                    if (!empty($this->serialize($child))) {
                        $localErrors[$key] = $this->buildErrorArray($error);
                    }
                }

                if (count($child->getIterator()) > 0 && ($child instanceof Form)) {
                    if (!empty($this->serialize($child))) {
                        $localErrors[$key] = $this->serialize($child);
                    }
                }
            }
        }

        return $localErrors;
    }

    /**
     * @param Form  $form
     * @param array $errors
     */
    public function unserialize(Form &$form, array $errors)
    {
        foreach ($errors as $field => $error) {
            if ($this->breakErrorLoop) {
                break;
            }

            if (count($errors) > 0 && !isset($error['message'])) {
                if ($this->isFieldset($form, $field) && !empty($error)) {
                    $form->get($field)->addError(
                        new FormError(
                            ucfirst(str_replace('_', ' ', $field)) . ' is invalid.'
                        )
                    );
                }

                $this->unserialize($form, $error);            
            } else {
                $this->searchForm($form, $field, $error);
            }
        }
    }

    /**
     * @param Form $form
     * @param      $field
     *
     * @return bool
     */
    private function isFieldset(Form $form, $field) : bool
    {
        if ($form->has($field)) {
            return $form->get($field)
                ->getConfig()
                ->getType()
                ->getInnerType() instanceof FieldsetComponentType;
        }

        return false;
    }

    /**
     * @param Form $form
     * @param      $needle
     * @param      $error
     *
     * @return void
     */
    private function searchForm(Form &$form, string $needle, array $error)
    {
        $searchKey = $form->getName();

        // If this is the root form then set the search key as the needle.
        if (!$form->getParent()) {
            $searchKey = $needle;
        }

        if ($form->has($needle) && $this->keyExists($this->originalFormErrors['fields'], $searchKey)) {
            $addError = true;

            foreach ($form->get($needle)->getErrors() as $item) {
                if ($item->getMessage() == $error['message']) {
                    $addError = false;
                }
            }

            if ($addError) {
                $formError = $this->createNewFormError($form, $error, $needle);
                $form->get($needle)->addError($formError);
            }
        } else {
            $originalFormErrors = $this->originalFormErrors;

            foreach ($form->getIterator() as $key => $child) {
                if (isset($this->originalFormErrors['fields'][$key])) {
                    $originalFormErrors = $this->originalFormErrors['fields'][$key];
                }

                if (!$child instanceof Form) {
                    continue;
                }

                $childIterator = $child->getIterator();

                $numItems = $childIterator->count();
                $counter = 0;

                foreach ($childIterator as $name => $field) {
                    if ($this->isTimestamp($name)) {
                        foreach ($originalFormErrors as $fieldName => $fieldValue) {
                            // PHP datetime needs to be divided by 1000 to match the unix time indexed field.
                            $epochName = str_replace('.', '', ($fieldName/1000));

                            if ($name == $epochName) {
                                if ($child->get($name)->getErrors()->count() === 0) {
                                    $formError = $this->createNewFormError($field, $error, $needle);
                                    if ($child->get($name)->has($needle)) {
                                        $child->get($name)->get($needle)->addError($formError);
                                    }
                                }
                            }
                        }

                        if (++$counter === $numItems) {
                            $this->breakErrorLoop = true;
                            break 2;
                        }
                    }
                }

                if ($form->get($key)->has($needle) && isset($originalFormErrors[$key])) {
                    $formError = $this->createNewFormError($child, $error, $needle);
                    $form->get($key)->get($needle)->addError($formError);

                    // Remove from original errors array.
                    unset($this->originalFormErrors['fields'][$key][$needle]);

                    break;
                }

                if (count($child->getIterator()) > 0 && ($child instanceof Form)) {
                    $this->searchForm($child, $needle, $error);
                }
            }
        }
    }

    /**
     * @param $timestamp
     *
     * @return bool
     */
    private function isTimestamp($timestamp) : bool
    {
        $check = (is_int($timestamp) OR is_float($timestamp))
            ? $timestamp
            : (string) (int) $timestamp;
        return  ($check === $timestamp)
            && ( (int) $timestamp <=  PHP_INT_MAX)
            && ( (int) $timestamp >= ~PHP_INT_MAX)
            && $check != 0;
    }

    /**
     * @param $errorIn
     *
     * @return array
     */
    private function buildErrorArray(FormError $errorIn) : array
    {
        $errorOut = [];

        $errorOut['message'] = $errorIn->getMessage();
        $errorOut['messageTemplate'] = $errorIn->getMessageTemplate();
        $errorOut['messagePluralization'] = $errorIn->getMessagePluralization();
        $errorOut['messageParameters'] = $errorIn->getMessageParameters();

        if ($errorCause = $errorIn->getCause()) {
            $errorOut['cause']['message'] = $errorCause->getMessage();
            $errorOut['cause']['messageTemplate'] = $errorCause->getMessageTemplate();
            $errorOut['cause']['parameters'] = $errorCause->getParameters();
            $errorOut['cause']['plural'] = $errorCause->getPlural();
            $errorOut['cause']['propertyPath'] = $errorCause->getPropertyPath();
            $errorOut['cause']['invalidValue'] = $errorCause->getInvalidValue();
            $errorOut['cause']['constraint'] = serialize($errorCause->getConstraint());
            $errorOut['cause']['code'] = $errorCause->getCode();
            $errorOut['cause']['cause'] = $errorCause->getCause();
        }

        return $errorOut;
    }

    /**
     * @param Form $form
     * @param      $error
     * @param      $field
     *
     * @return FormError
     */
    private function createNewFormError(Form $form, $error, $field) : FormError
    {
        if (isset($error['cause']['cause'])) {
            $error['cause']['propertyPath'] =
                $error['cause']['cause']['propertyPath'] ?? 'children[' . $field . '].data';
            $error['cause']['invalidValue'] = $error['cause']['cause']['invalidValue'] ?? null;
            $error['cause']['plural'] = $error['cause']['cause']['plural'] ?? null;
            $error['cause']['code'] = $error['cause']['cause']['code'] ?? null;
            $error['cause']['constraint'] = $error['cause']['cause']['constraint'] ?? null;
        }

        $constraint = null;

        if (isset($error['cause']['constraint'])) {
            $constraint = unserialize($error['cause']['constraint']);
        }

        return new FormError(
            $error['message'],
            $error['messageTemplate'] ?? null,
            $error['messageParameters'] ?? [],
            $error['messagePluralization'] ?? null,
            new ConstraintViolation(
                $error['cause']['message'],
                $error['cause']['messageTemplate'],
                $error['cause']['parameters'],
                $form,
                $error['cause']['propertyPath'] ?? $field . '.data',
                $error['cause']['invalidValue'] ?? '',
                $error['cause']['plural'] ?? null,
                $error['cause']['code'] ?? null,
                $constraint,
                $error['cause']['cause'] ?? null
            )
        );
    }

    /**
     * @param        $array
     * @param string $separator
     * @param string $flattenedKey
     *
     * @return array
     */
    private function arrayFlatten(array $array, string $separator = '_', string $flattenedKey = '') : array
    {
        $flattenedArray = [];

        foreach ($array as $key => $value) {
            if (is_array($value)) {
                $flattenedArray = array_merge(
                    $flattenedArray,
                    $this->arrayFlatten(
                        $value,
                        $separator,
                        (
                        strlen($flattenedKey) > 0 ? $flattenedKey . $separator : ""
                        ) . $key
                    )
                );
            } else {
                $flattenedArray[(strlen($flattenedKey) > 0 ? $flattenedKey . $separator : "") . $key] = $value;
            }
        }

        return $flattenedArray;
    }

    /**
     * @param $array
     * @param $keySearch
     *
     * @return bool
     */
    protected function keyExists($array, $keySearch)
    {
        foreach ($array as $key => $item) {
            if (
                $key == $keySearch ||
                (is_array($item) && $this->keyExists($item, $keySearch))
            ) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param array $errors
     *
     * @return FormErrorsSerializer
     */
    public function setOriginalFormErrors(array $errors) : FormErrorsSerializer
    {
        $this->originalFormErrors = $errors;

        return $this;
    }

    /**
     * @return array
     */
    public function getOriginalFormErrors() : array
    {
        return $this->originalFormErrors;
    }
}

Please note that all pasted data is publicly available.