<NME> Save.php
<BEF> <?php

namespace Bolt\Storage\ContentRequest;

use Bolt\Config;
use Bolt\Exception\AccessControlException;
use Bolt\Exception\InvalidRepositoryException;
use Bolt\Helpers\Input;
use Bolt\Helpers\Str;
use Bolt\Logger\FlashLoggerInterface;
use Bolt\Storage\Collection;
use Bolt\Storage\Entity;
use Bolt\Storage\EntityManager;
use Bolt\Storage\Mapping\ContentType;
use Bolt\Translation\Translator as Trans;
use Bolt\Users;
use Carbon\Carbon;
use Cocur\Slugify\Slugify;
use Cocur\Slugify\SlugifyInterface;
use Psr\Log\LoggerInterface;
use RecursiveArrayIterator;
use RecursiveIteratorIterator;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/**
 * Helper class for ContentType record editor saves.
 *
 * Prior to v3.0 this functionality existed in \Bolt\Controllers\Backend::editcontent().
 *
 * @author Gawain Lynch <gawain.lynch@gmail.com>
 */
class Save
{
    /** @var EntityManager */
    protected $em;
    /** @var Config */
    protected $config;
    /** @var Users */
    protected $users;
    /** @var LoggerInterface */
    protected $loggerChange;
    /** @var LoggerInterface */
    protected $loggerSystem;
    /** @var FlashLoggerInterface */
    protected $loggerFlash;
    /** @var UrlGeneratorInterface */
    protected $urlGenerator;
    /** @var SlugifyInterface */
    private $slugify;

    /**
     * Constructor function.
     *
     * @param EntityManager         $em
     * @param Config                $config
     * @param Users                 $users
     * @param LoggerInterface       $loggerChange
     * @param LoggerInterface       $loggerSystem
     * @param FlashLoggerInterface  $loggerFlash
     * @param UrlGeneratorInterface $urlGenerator
     * @param SlugifyInterface      $slugify
     */
    public function __construct(
        EntityManager $em,
        Config $config,
        Users $users,
        LoggerInterface $loggerChange,
        LoggerInterface $loggerSystem,
        FlashLoggerInterface $loggerFlash,
        UrlGeneratorInterface $urlGenerator,
        SlugifyInterface $slugify = null
    ) {
        $this->em = $em;
        $this->config = $config;
        $this->users = $users;
        $this->loggerChange = $loggerChange;
        $this->loggerSystem = $loggerSystem;
        $this->loggerFlash = $loggerFlash;
        $this->urlGenerator = $urlGenerator;
        $this->slugify = $slugify;
    }

    /**
     * Do the save for a POSTed record.
     *
     * @param array $formValues
     * @param array|ContentType $contentType The ContentType data
     * @param int $id The record ID
     * @param bool $new If TRUE this is a new record
     * @param string $returnTo
     * @param string $editReferrer
     *
     * @throws AccessControlException
     * @throws InvalidRepositoryException
     *
     * @return Response
     */
    public function action(array $formValues, $contentType, $id, $new, $returnTo, $editReferrer)
    {
        $contentTypeSlug = $contentType['slug'];
        $repo = $this->em->getRepository($contentTypeSlug);

        // If we have an ID now, this is an existing record
        if ($id) {
            $content = $repo->find($id);
            $oldContent = clone $content;
            $oldStatus = $content['status'];
        } else {
            $content = $repo->create(['contenttype' => $contentTypeSlug, 'status' => $contentType['default_status']]);
            $oldContent = null;
            $oldStatus = 'draft';
        }

        // Don't allow spoofing the ID.
        if ($content->getId() !== null && (int) $id !== $content->getId()) {
            if ($returnTo === 'ajax') {
                throw new AccessControlException("Don't try to spoof the id!");
            }
            $this->loggerFlash->error("Don't try to spoof the id!");

            return new RedirectResponse($this->generateUrl('dashboard'));
        }

        $this->setPostedValues($content, $formValues, $contentType);
        $this->setTransitionStatus($content, $contentTypeSlug, $id, $oldStatus);

        // Get the associated record change comment
        $comment = isset($formValues['changelog-comment']) ? $formValues['changelog-comment'] : '';

        // Save the record
        return $this->saveContentRecord($content, $oldContent, $contentType, $new, $comment, $returnTo, $editReferrer);
    }

    /**
     * Check whether the status is allowed.
     *
     * We act as if a status *transition* were requested and fallback to the old
     * status otherwise.
     *
     * @param Entity\Content $content
     * @param string         $contentTypeSlug
     * @param int            $id
     * @param string         $oldStatus
     */
    private function setTransitionStatus(Entity\Content $content, $contentTypeSlug, $id, $oldStatus)
    {
        $canTransition = $this->users->isContentStatusTransitionAllowed($oldStatus, $content->getStatus(), $contentTypeSlug, $id);
        if (!$canTransition) {
            $content->setStatus($oldStatus);
        }
    }

    /**
     * Set a ContentType record values from a HTTP POST.
     *
     * @param Entity\Content $content
     * @param array          $formValues
     * @param array          $contentType
     *
     * @throws AccessControlException
     */
    private function setPostedValues(Entity\Content $content, $formValues, $contentType)
    {
        // Ensure all fields have valid values
        $formValues = $this->setSuccessfulControlValues($formValues, $contentType['fields']);
        $formValues = Input::cleanPostedData($formValues);
        unset($formValues['contenttype']);

        $user = $this->users->getCurrentUser();
        if ($id = $content->getId()) {
            // Owner is set explicitly, is current user is allowed to do this?
            if (isset($formValues['ownerid']) && (int) $formValues['ownerid'] !== $content->getOwnerid()) {
                if (!$this->users->isAllowed("contenttype:{$contentType['slug']}:change-ownership:$id")) {
                    throw new AccessControlException('Changing ownership is not allowed.');
                }
                $content->setOwnerid($formValues['ownerid']);
            }
        } else {
            $content->setOwnerid($user['id']);
        }

        // Hack … remove soon
        $formValues += ['status' => 'draft'];
        // Make sure we have a proper status.
        if (!in_array($formValues['status'], ['published', 'timed', 'held', 'draft'])) {
            if ($status = $content->getStatus()) {
                $formValues['status'] = $status;
            } else {
                $formValues['status'] = 'draft';
            }
        }

        // Set the object values appropriately
        foreach ($formValues as $name => $value) {
            if ($name === 'relation' || $name === 'taxonomy') {
                continue;
            }
            $content->set($name, (empty($value) || $this->isEmptyArray($value)) ? null : $value);
        }
        foreach ($contentType['fields'] as $fieldName => $data) {
            if ($data['type'] !== 'slug' || !isset($formValues[$fieldName])) {
                continue;
            }
            if ($this->slugify !== null) {
                $snail = $this->slugify->slugify($formValues[$fieldName]);
            } else {
                // @deprecated Required for BC overrides
                $snail = Slugify::create()->slugify($formValues[$fieldName]);
            }
            $content->set($fieldName, $snail);
        }

        $this->setPostedRelations($content, $formValues);
        $this->setPostedTaxonomies($content, $formValues);
    }

    /**
     * Convert POST relationship values to an array of Entity objects keyed by
     * ContentType.
     *
     * @param Entity\Content $content
     * @param array|null     $formValues
     */
    private function setPostedRelations(Entity\Content $content, $formValues)
    {
        /** @var Collection\Relations $related */
        $related = $this->em->createCollection(Entity\Relations::class);
        $related->setFromPost($formValues, $content);
        $content->setRelation($related);
    }

    /**
     * Set valid POST taxonomies.
     *
     * @param Entity\Content $content
     * @param array|null     $formValues
     */
    private function setPostedTaxonomies(Entity\Content $content, $formValues)
    {
        /** @var Collection\Taxonomy $taxonomies */
        $taxonomies = $this->em->createCollection(Entity\Taxonomy::class);
        $taxonomies->setFromPost($formValues, $content);
        $content->setTaxonomy($taxonomies);
    }

    /**
     * Commit the record to the database.
     *
     * @param Entity\Content $content
     * @param Entity\Content|null $oldContent
     * @param array|ContentType $contentType
     * @param bool $new
     * @param string $comment
     * @param string $returnTo
     * @param string $editReferrer
     *
     * @return Response|null
     * @throws InvalidRepositoryException
     */
    private function saveContentRecord(Entity\Content $content, $oldContent, $contentType, $new, $comment, $returnTo, $editReferrer)
    {
        // Save the record
        $repo = $this->em->getRepository($contentType['slug']);

        // Update the date modified timestamp
        $content->setDatechanged('now');

        $repo->save($content);
        $id = $content->getId();

        // Create the change log entry if configured
        $this->logChange($contentType, $content->getId(), $content, $oldContent, $comment);
        $sanitizedTitle = Str::makeSafe($content->getTitle(), false, '()[]!@$%&*~`^-_=+{},.~<>:; /?');

        // Log the change
        if ($new) {
            $this->loggerFlash->success(Trans::__('contenttypes.generic.saved-new', ['%contenttype%' => $contentType['singular_name']]));
            $this->loggerSystem->info('Created: ' . $sanitizedTitle, ['event' => 'content']);
        } else {
            $this->loggerFlash->success(Trans::__('contenttypes.generic.saved-changes', ['%contenttype%' => $contentType['singular_name']]));
            $this->loggerSystem->info('Saved: ' . $sanitizedTitle, ['event' => 'content']);
        }

        if ($new && ($returnTo === 'save')) {
            $redirectUri = $this->generateUrl(
                'editcontent',
                [
                    'contenttypeslug' => $contentType['slug'],
                    'id'              => $id,
                ]
            );

            return new RedirectResponse($redirectUri);
        } elseif ($returnTo === 'ajax') {
            return $this->createJsonUpdate($content, true);
        } elseif ($returnTo === 'save_create') {
            $redirectUri = $this->generateUrl(
                'editcontent',
                [
                    'contenttypeslug' => $contentType['slug'],
                    '_fragment'       => $returnTo,
                ]
            );

            return new RedirectResponse($redirectUri);
        } elseif ($returnTo === 'save_return') {
            // No returnto, so we go back to the 'overview' for this contenttype.
            // check if a pager was set in the referrer - if yes go back there
            if ($editReferrer) {
                return new RedirectResponse($editReferrer);
            }

            $redirectUri = $this->generateUrl(
                'overview',
                [
                    $formValues[$key] = [];
                } elseif ($values['type'] === 'checkbox') {
                    $formValues[$key] = 0;
                }
            }
        }

        return null;
    }

    /**
     * Add successful control values to request values, and do needed corrections.
     *
     * @see http://www.w3.org/TR/html401/interact/forms.html#h-17.13.2
     *
     * @param array $formValues
     * @param array $fields
     *
     * @return array
     */
    private function setSuccessfulControlValues(array $formValues, $fields)
    {
        foreach ($fields as $key => $values) {
            if (isset($formValues[$key])) {
                if ($values['type'] === 'float') {
                    // We allow ',' and '.' as decimal point and need '.' internally
                    $formValues[$key] = str_replace(',', '.', $formValues[$key]);
                }
            } else {
                if ($values['type'] === 'select' && isset($values['multiple']) && $values['multiple'] === true) {
                    $formValues[$key] = [];
                } elseif ($values['type'] === 'checkbox') {
                    $formValues[$key] = 0;
                } elseif ($values['type'] === 'repeater' || $values['type'] === 'block') {
                    $formValues[$key] = [];
                }
            }
        }

        return $formValues;
    }

    /**
     * Build a valid AJAX response for in-place saves that account for pre/post
     * save events.
     *
     * @param Entity\Content $content
     * @param bool           $flush
     *
     * @return JsonResponse
     */
    private function createJsonUpdate(Entity\Content $content, $flush)
    {
        /*
         * Flush any buffers from saveConent() dispatcher hooks
         * and make sure our JSON output is clean.
         *
         * Currently occurs due to exceptions being generated in the dispatchers
         * in \Bolt\Storage::saveContent()
         *     StorageEvents::PRE_SAVE
         *     StorageEvents::POST_SAVE
         */
        if ($flush) {
            Response::closeOutputBuffers(0, false);
        }

        $val = $content->toArray();

        if ($val['datechanged'] instanceof Carbon) {
            $val['datechanged'] = $val['datechanged']->toIso8601String();
        } elseif (isset($val['datechanged'])) {
            $val['datechanged'] = (new Carbon($val['datechanged']))->toIso8601String();
        }

        // Adjust decimal point as some locales use a comma and… JavaScript
        $lc = localeconv();
        $fields = $this->config->get('contenttypes/' . $content->getContenttype() . '/fields');
        foreach ($fields as $key => $values) {
            if ($values['type'] === 'float' && $lc['decimal_point'] === ',') {
                $val[$key] = str_replace('.', ',', $val[$key]);
            }
        }

        // Unset flashbag for ajax
        $this->loggerFlash->clear();

        return new JsonResponse($val);
    }

    /**
     * Add a change log entry to track the change.
     *
     * @param string              $contentType
     * @param int                 $contentId
     * @param Entity\Content      $newContent
     * @param Entity\Content|null $oldContent
     * @param string|null         $comment
     */
    private function logChange($contentType, $contentId, $newContent = null, $oldContent = null, $comment = null)
    {
        $type = $oldContent ? 'Update' : 'Insert';

        $this->loggerChange->info(
            $type . ' record',
            [
                'action'      => strtoupper($type),
                'contenttype' => $contentType,
                'id'          => $contentId,
                'new'         => $newContent ? $newContent->toArray() : null,
                'old'         => $oldContent ? $oldContent->toArray() : null,
                'comment'     => $comment,
            ]
        );
    }

    /**
     * Shortcut for {@see UrlGeneratorInterface::generate}.
     *
     * @param string $name          The name of the route
     * @param array  $params        An array of parameters
     * @param int    $referenceType The type of reference to be generated (one of the constants)
     *
     * @return string
     */
    private function generateUrl($name, $params = [], $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
    {
        /** @var UrlGeneratorInterface $generator */
        $generator = $this->urlGenerator;

        return $generator->generate($name, $params, $referenceType);
    }

    /**
     * Check wether an array is empty and if it is the value of the repeater is set to null.
     *
     * @param array $input
     *
     * @return bool
     */
    private function isEmptyArray($input)
    {
        if (!is_array($input)) {
            return false;
        }
        $empty = true;
        foreach (new RecursiveIteratorIterator(new RecursiveArrayIterator($input), RecursiveIteratorIterator::LEAVES_ONLY) as $key => $value) {
            $empty = (empty($value) && $value !== '0' && $value !== 0);
            if (!$empty) {
                return false;
            }
        }

        return $empty;
    }
}

<MSG> when no repeater values are posted, make sure that the field is set to empty

<DFF> @@ -319,6 +319,8 @@ class Save
                     $formValues[$key] = [];
                 } elseif ($values['type'] === 'checkbox') {
                     $formValues[$key] = 0;
+                } elseif ($values['type'] === 'repeater' || $values['type'] === 'block') {
+                    $formValues[$key] = [];
                 }
             }
         }

{'commit': "diff --git a/src/Storage/ContentRequest/Save.php b/src/Storage/ContentRequest/Save.php\nindex 6835be543..117cf4e13 100644\n--- a/src/Storage/ContentRequest/Save.php\n+++ b/src/Storage/ContentRequest/Save.php\n@@ -319,6 +319,8 @@ class Save\n                     $formValues[$key] = [];\n                 } elseif ($values['type'] === 'checkbox') {\n                     $formValues[$key] = 0;\n+                } elseif ($values['type'] === 'repeater' || $values['type'] === 'block') {\n+                    $formValues[$key] = [];\n                 }\n             }\n         }\n", 'commit_subject': 'when no repeater values are posted, make sure that the field is set to empty', 'repo_name': 'bolt/bolt', 'committer': {'name': 'Ross Riley', 'email': 'eebce68ab99567722cee5644714a63dca807990d@gmail.com', 'time_sec': '1485677072', 'tz_offset': '0', 'date': {'seconds': '1485677072'}}, 'commit_tree': '3a40bc170cc7f83a984f75967b36bfd952bbb1b0', 'commit_message': 'when no repeater values are posted, make sure that the field is set to empty\n', 'language_name': 'PHP', 'license': 'mit', 'hunks': "@@ -319,6 +319,8 @@ class Save\n                     $formValues[$key] = [];\n                 } elseif ($values['type'] === 'checkbox') {\n                     $formValues[$key] = 0;\n+                } elseif ($values['type'] === 'repeater' || $values['type'] === 'block') {\n+                    $formValues[$key] = [];\n                 }\n             }\n         }\n", 'addition_count': 2, 'deletion_count': 0, 'src_file': 'a/src/Storage/ContentRequest/Save.php', 'tgt_file': 'b/src/Storage/ContentRequest/Save.php', 'file_extension': '.php', 'before_file': '<?php\n\nnamespace Bolt\\Storage\\ContentRequest;\n\nuse Bolt\\Config;\nuse Bolt\\Exception\\AccessControlException;\nuse Bolt\\Exception\\InvalidRepositoryException;\nuse Bolt\\Helpers\\Input;\nuse Bolt\\Helpers\\Str;\nuse Bolt\\Logger\\FlashLoggerInterface;\nuse Bolt\\Storage\\Collection;\nuse Bolt\\Storage\\Entity;\nuse Bolt\\Storage\\EntityManager;\nuse Bolt\\Storage\\Mapping\\ContentType;\nuse Bolt\\Translation\\Translator as Trans;\nuse Bolt\\Users;\nuse Carbon\\Carbon;\nuse Cocur\\Slugify\\Slugify;\nuse Cocur\\Slugify\\SlugifyInterface;\nuse Psr\\Log\\LoggerInterface;\nuse RecursiveArrayIterator;\nuse RecursiveIteratorIterator;\nuse Symfony\\Component\\HttpFoundation\\JsonResponse;\nuse Symfony\\Component\\HttpFoundation\\RedirectResponse;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface;\n\n/**\n * Helper class for ContentType record editor saves.\n *\n * Prior to v3.0 this functionality existed in \\Bolt\\Controllers\\Backend::editcontent().\n *\n * @author Gawain Lynch <gawain.lynch@gmail.com>\n */\nclass Save\n{\n    /** @var EntityManager */\n    protected $em;\n    /** @var Config */\n    protected $config;\n    /** @var Users */\n    protected $users;\n    /** @var LoggerInterface */\n    protected $loggerChange;\n    /** @var LoggerInterface */\n    protected $loggerSystem;\n    /** @var FlashLoggerInterface */\n    protected $loggerFlash;\n    /** @var UrlGeneratorInterface */\n    protected $urlGenerator;\n    /** @var SlugifyInterface */\n    private $slugify;\n\n    /**\n     * Constructor function.\n     *\n     * @param EntityManager         $em\n     * @param Config                $config\n     * @param Users                 $users\n     * @param LoggerInterface       $loggerChange\n     * @param LoggerInterface       $loggerSystem\n     * @param FlashLoggerInterface  $loggerFlash\n     * @param UrlGeneratorInterface $urlGenerator\n     * @param SlugifyInterface      $slugify\n     */\n    public function __construct(\n        EntityManager $em,\n        Config $config,\n        Users $users,\n        LoggerInterface $loggerChange,\n        LoggerInterface $loggerSystem,\n        FlashLoggerInterface $loggerFlash,\n        UrlGeneratorInterface $urlGenerator,\n        SlugifyInterface $slugify = null\n    ) {\n        $this->em = $em;\n        $this->config = $config;\n        $this->users = $users;\n        $this->loggerChange = $loggerChange;\n        $this->loggerSystem = $loggerSystem;\n        $this->loggerFlash = $loggerFlash;\n        $this->urlGenerator = $urlGenerator;\n        $this->slugify = $slugify;\n    }\n\n    /**\n     * Do the save for a POSTed record.\n     *\n     * @param array $formValues\n     * @param array|ContentType $contentType The ContentType data\n     * @param int $id The record ID\n     * @param bool $new If TRUE this is a new record\n     * @param string $returnTo\n     * @param string $editReferrer\n     *\n     * @throws AccessControlException\n     * @throws InvalidRepositoryException\n     *\n     * @return Response\n     */\n    public function action(array $formValues, $contentType, $id, $new, $returnTo, $editReferrer)\n    {\n        $contentTypeSlug = $contentType[\'slug\'];\n        $repo = $this->em->getRepository($contentTypeSlug);\n\n        // If we have an ID now, this is an existing record\n        if ($id) {\n            $content = $repo->find($id);\n            $oldContent = clone $content;\n            $oldStatus = $content[\'status\'];\n        } else {\n            $content = $repo->create([\'contenttype\' => $contentTypeSlug, \'status\' => $contentType[\'default_status\']]);\n            $oldContent = null;\n            $oldStatus = \'draft\';\n        }\n\n        // Don\'t allow spoofing the ID.\n        if ($content->getId() !== null && (int) $id !== $content->getId()) {\n            if ($returnTo === \'ajax\') {\n                throw new AccessControlException("Don\'t try to spoof the id!");\n            }\n            $this->loggerFlash->error("Don\'t try to spoof the id!");\n\n            return new RedirectResponse($this->generateUrl(\'dashboard\'));\n        }\n\n        $this->setPostedValues($content, $formValues, $contentType);\n        $this->setTransitionStatus($content, $contentTypeSlug, $id, $oldStatus);\n\n        // Get the associated record change comment\n        $comment = isset($formValues[\'changelog-comment\']) ? $formValues[\'changelog-comment\'] : \'\';\n\n        // Save the record\n        return $this->saveContentRecord($content, $oldContent, $contentType, $new, $comment, $returnTo, $editReferrer);\n    }\n\n    /**\n     * Check whether the status is allowed.\n     *\n     * We act as if a status *transition* were requested and fallback to the old\n     * status otherwise.\n     *\n     * @param Entity\\Content $content\n     * @param string         $contentTypeSlug\n     * @param int            $id\n     * @param string         $oldStatus\n     */\n    private function setTransitionStatus(Entity\\Content $content, $contentTypeSlug, $id, $oldStatus)\n    {\n        $canTransition = $this->users->isContentStatusTransitionAllowed($oldStatus, $content->getStatus(), $contentTypeSlug, $id);\n        if (!$canTransition) {\n            $content->setStatus($oldStatus);\n        }\n    }\n\n    /**\n     * Set a ContentType record values from a HTTP POST.\n     *\n     * @param Entity\\Content $content\n     * @param array          $formValues\n     * @param array          $contentType\n     *\n     * @throws AccessControlException\n     */\n    private function setPostedValues(Entity\\Content $content, $formValues, $contentType)\n    {\n        // Ensure all fields have valid values\n        $formValues = $this->setSuccessfulControlValues($formValues, $contentType[\'fields\']);\n        $formValues = Input::cleanPostedData($formValues);\n        unset($formValues[\'contenttype\']);\n\n        $user = $this->users->getCurrentUser();\n        if ($id = $content->getId()) {\n            // Owner is set explicitly, is current user is allowed to do this?\n            if (isset($formValues[\'ownerid\']) && (int) $formValues[\'ownerid\'] !== $content->getOwnerid()) {\n                if (!$this->users->isAllowed("contenttype:{$contentType[\'slug\']}:change-ownership:$id")) {\n                    throw new AccessControlException(\'Changing ownership is not allowed.\');\n                }\n                $content->setOwnerid($formValues[\'ownerid\']);\n            }\n        } else {\n            $content->setOwnerid($user[\'id\']);\n        }\n\n        // Hack … remove soon\n        $formValues += [\'status\' => \'draft\'];\n        // Make sure we have a proper status.\n        if (!in_array($formValues[\'status\'], [\'published\', \'timed\', \'held\', \'draft\'])) {\n            if ($status = $content->getStatus()) {\n                $formValues[\'status\'] = $status;\n            } else {\n                $formValues[\'status\'] = \'draft\';\n            }\n        }\n\n        // Set the object values appropriately\n        foreach ($formValues as $name => $value) {\n            if ($name === \'relation\' || $name === \'taxonomy\') {\n                continue;\n            }\n            $content->set($name, (empty($value) || $this->isEmptyArray($value)) ? null : $value);\n        }\n        foreach ($contentType[\'fields\'] as $fieldName => $data) {\n            if ($data[\'type\'] !== \'slug\' || !isset($formValues[$fieldName])) {\n                continue;\n            }\n            if ($this->slugify !== null) {\n                $snail = $this->slugify->slugify($formValues[$fieldName]);\n            } else {\n                // @deprecated Required for BC overrides\n                $snail = Slugify::create()->slugify($formValues[$fieldName]);\n            }\n            $content->set($fieldName, $snail);\n        }\n\n        $this->setPostedRelations($content, $formValues);\n        $this->setPostedTaxonomies($content, $formValues);\n    }\n\n    /**\n     * Convert POST relationship values to an array of Entity objects keyed by\n     * ContentType.\n     *\n     * @param Entity\\Content $content\n     * @param array|null     $formValues\n     */\n    private function setPostedRelations(Entity\\Content $content, $formValues)\n    {\n        /** @var Collection\\Relations $related */\n        $related = $this->em->createCollection(Entity\\Relations::class);\n        $related->setFromPost($formValues, $content);\n        $content->setRelation($related);\n    }\n\n    /**\n     * Set valid POST taxonomies.\n     *\n     * @param Entity\\Content $content\n     * @param array|null     $formValues\n     */\n    private function setPostedTaxonomies(Entity\\Content $content, $formValues)\n    {\n        /** @var Collection\\Taxonomy $taxonomies */\n        $taxonomies = $this->em->createCollection(Entity\\Taxonomy::class);\n        $taxonomies->setFromPost($formValues, $content);\n        $content->setTaxonomy($taxonomies);\n    }\n\n    /**\n     * Commit the record to the database.\n     *\n     * @param Entity\\Content $content\n     * @param Entity\\Content|null $oldContent\n     * @param array|ContentType $contentType\n     * @param bool $new\n     * @param string $comment\n     * @param string $returnTo\n     * @param string $editReferrer\n     *\n     * @return Response|null\n     * @throws InvalidRepositoryException\n     */\n    private function saveContentRecord(Entity\\Content $content, $oldContent, $contentType, $new, $comment, $returnTo, $editReferrer)\n    {\n        // Save the record\n        $repo = $this->em->getRepository($contentType[\'slug\']);\n\n        // Update the date modified timestamp\n        $content->setDatechanged(\'now\');\n\n        $repo->save($content);\n        $id = $content->getId();\n\n        // Create the change log entry if configured\n        $this->logChange($contentType, $content->getId(), $content, $oldContent, $comment);\n        $sanitizedTitle = Str::makeSafe($content->getTitle(), false, \'()[]!@$%&*~`^-_=+{},.~<>:; /?\');\n\n        // Log the change\n        if ($new) {\n            $this->loggerFlash->success(Trans::__(\'contenttypes.generic.saved-new\', [\'%contenttype%\' => $contentType[\'singular_name\']]));\n            $this->loggerSystem->info(\'Created: \' . $sanitizedTitle, [\'event\' => \'content\']);\n        } else {\n            $this->loggerFlash->success(Trans::__(\'contenttypes.generic.saved-changes\', [\'%contenttype%\' => $contentType[\'singular_name\']]));\n            $this->loggerSystem->info(\'Saved: \' . $sanitizedTitle, [\'event\' => \'content\']);\n        }\n\n        if ($new && ($returnTo === \'save\')) {\n            $redirectUri = $this->generateUrl(\n                \'editcontent\',\n                [\n                    \'contenttypeslug\' => $contentType[\'slug\'],\n                    \'id\'              => $id,\n                ]\n            );\n\n            return new RedirectResponse($redirectUri);\n        } elseif ($returnTo === \'ajax\') {\n            return $this->createJsonUpdate($content, true);\n        } elseif ($returnTo === \'save_create\') {\n            $redirectUri = $this->generateUrl(\n                \'editcontent\',\n                [\n                    \'contenttypeslug\' => $contentType[\'slug\'],\n                    \'_fragment\'       => $returnTo,\n                ]\n            );\n\n            return new RedirectResponse($redirectUri);\n        } elseif ($returnTo === \'save_return\') {\n            // No returnto, so we go back to the \'overview\' for this contenttype.\n            // check if a pager was set in the referrer - if yes go back there\n            if ($editReferrer) {\n                return new RedirectResponse($editReferrer);\n            }\n\n            $redirectUri = $this->generateUrl(\n                \'overview\',\n                [\n                    $formValues[$key] = [];\n                } elseif ($values[\'type\'] === \'checkbox\') {\n                    $formValues[$key] = 0;\n                }\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Add successful control values to request values, and do needed corrections.\n     *\n     * @see http://www.w3.org/TR/html401/interact/forms.html#h-17.13.2\n     *\n     * @param array $formValues\n     * @param array $fields\n     *\n     * @return array\n     */\n    private function setSuccessfulControlValues(array $formValues, $fields)\n    {\n        foreach ($fields as $key => $values) {\n            if (isset($formValues[$key])) {\n                if ($values[\'type\'] === \'float\') {\n                    // We allow \',\' and \'.\' as decimal point and need \'.\' internally\n                    $formValues[$key] = str_replace(\',\', \'.\', $formValues[$key]);\n                }\n            } else {\n                if ($values[\'type\'] === \'select\' && isset($values[\'multiple\']) && $values[\'multiple\'] === true) {\n                    $formValues[$key] = [];\n                } elseif ($values[\'type\'] === \'checkbox\') {\n                    $formValues[$key] = 0;\n                } elseif ($values[\'type\'] === \'repeater\' || $values[\'type\'] === \'block\') {\n                    $formValues[$key] = [];\n                }\n            }\n        }\n\n        return $formValues;\n    }\n\n    /**\n     * Build a valid AJAX response for in-place saves that account for pre/post\n     * save events.\n     *\n     * @param Entity\\Content $content\n     * @param bool           $flush\n     *\n     * @return JsonResponse\n     */\n    private function createJsonUpdate(Entity\\Content $content, $flush)\n    {\n        /*\n         * Flush any buffers from saveConent() dispatcher hooks\n         * and make sure our JSON output is clean.\n         *\n         * Currently occurs due to exceptions being generated in the dispatchers\n         * in \\Bolt\\Storage::saveContent()\n         *     StorageEvents::PRE_SAVE\n         *     StorageEvents::POST_SAVE\n         */\n        if ($flush) {\n            Response::closeOutputBuffers(0, false);\n        }\n\n        $val = $content->toArray();\n\n        if ($val[\'datechanged\'] instanceof Carbon) {\n            $val[\'datechanged\'] = $val[\'datechanged\']->toIso8601String();\n        } elseif (isset($val[\'datechanged\'])) {\n            $val[\'datechanged\'] = (new Carbon($val[\'datechanged\']))->toIso8601String();\n        }\n\n        // Adjust decimal point as some locales use a comma and… JavaScript\n        $lc = localeconv();\n        $fields = $this->config->get(\'contenttypes/\' . $content->getContenttype() . \'/fields\');\n        foreach ($fields as $key => $values) {\n            if ($values[\'type\'] === \'float\' && $lc[\'decimal_point\'] === \',\') {\n                $val[$key] = str_replace(\'.\', \',\', $val[$key]);\n            }\n        }\n\n        // Unset flashbag for ajax\n        $this->loggerFlash->clear();\n\n        return new JsonResponse($val);\n    }\n\n    /**\n     * Add a change log entry to track the change.\n     *\n     * @param string              $contentType\n     * @param int                 $contentId\n     * @param Entity\\Content      $newContent\n     * @param Entity\\Content|null $oldContent\n     * @param string|null         $comment\n     */\n    private function logChange($contentType, $contentId, $newContent = null, $oldContent = null, $comment = null)\n    {\n        $type = $oldContent ? \'Update\' : \'Insert\';\n\n        $this->loggerChange->info(\n            $type . \' record\',\n            [\n                \'action\'      => strtoupper($type),\n                \'contenttype\' => $contentType,\n                \'id\'          => $contentId,\n                \'new\'         => $newContent ? $newContent->toArray() : null,\n                \'old\'         => $oldContent ? $oldContent->toArray() : null,\n                \'comment\'     => $comment,\n            ]\n        );\n    }\n\n    /**\n     * Shortcut for {@see UrlGeneratorInterface::generate}.\n     *\n     * @param string $name          The name of the route\n     * @param array  $params        An array of parameters\n     * @param int    $referenceType The type of reference to be generated (one of the constants)\n     *\n     * @return string\n     */\n    private function generateUrl($name, $params = [], $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)\n    {\n        /** @var UrlGeneratorInterface $generator */\n        $generator = $this->urlGenerator;\n\n        return $generator->generate($name, $params, $referenceType);\n    }\n\n    /**\n     * Check wether an array is empty and if it is the value of the repeater is set to null.\n     *\n     * @param array $input\n     *\n     * @return bool\n     */\n    private function isEmptyArray($input)\n    {\n        if (!is_array($input)) {\n            return false;\n        }\n        $empty = true;\n        foreach (new RecursiveIteratorIterator(new RecursiveArrayIterator($input), RecursiveIteratorIterator::LEAVES_ONLY) as $key => $value) {\n            $empty = (empty($value) && $value !== \'0\' && $value !== 0);\n            if (!$empty) {\n                return false;\n            }\n        }\n\n        return $empty;\n    }\n}\n', 'message': 'when no repeater values are posted, make sure that the field is set to empty\n', 'author_name': 'Ross Riley', 'lang': 'php'}