Avoiding Save in a Loop in Magento 2 Mass Action

best practicecoding-standardscrudmagento2save

I've created my own CRUD module that contains an inline edit action similar to the one for CMS pages
Everything works OK, but when running phpsniffer with the EcgM2 standard I get this warning:

Model LSD method save() detected in loop

How can I avoid this?
Note: The same warning appears if I "sniff" the core file linked above.
Here is my execute method in case someone needs it. But it's very similar to the one from the CMS page controller

public function execute()
{
    /** @var \Magento\Framework\Controller\Result\Json $resultJson */
    $resultJson = $this->jsonFactory->create();
    $error = false;
    $messages = [];

    $postItems = $this->getRequest()->getParam('items', []);
    if (!($this->getRequest()->getParam('isAjax') && count($postItems))) {
        return $resultJson->setData([
            'messages' => [__('Please correct the data sent.')],
            'error' => true,
        ]);
    }

    foreach (array_keys($postItems) as $authorId) {
        /** @var \Sample\News\Model\Author $author */
        $author = $this->authorRepository->getById((int)$authorId);
        try {
            $authorData = $this->filterData($postItems[$authorId]);
            $this->dataObjectHelper->populateWithArray($author, $authorData , AuthorInterface::class);
            $this->authorRepository->save($author);
        } catch (LocalizedException $e) {
            $messages[] = $this->getErrorWithAuthorId($author, $e->getMessage());
            $error = true;
        } catch (\RuntimeException $e) {
            $messages[] = $this->getErrorWithAuthorId($author, $e->getMessage());
            $error = true;
        } catch (\Exception $e) {
            $messages[] = $this->getErrorWithAuthorId(
                $author,
                __('Something went wrong while saving the author.')
            );
            $error = true;
        }
    }

    return $resultJson->setData([
        'messages' => $messages,
        'error' => $error
    ]);
}

Best Answer

In that case you have to save() your entities, so you'll definitely have to call that method.

The native core Magento file you linked is not the only one that does that, especially the mass actions action classes.

The only alternative is to add a saveAttribute method to your CRUD resource model based on the one implemented in app/code/Magento/Sales/Model/ResourceModel/Attribute.php :

public function saveAttribute(AbstractModel $object, $attribute)
{
    if ($attribute instanceof AbstractAttribute) {
        $attributes = $attribute->getAttributeCode();
    } elseif (is_string($attribute)) {
        $attributes = [$attribute];
    } else {
        $attributes = $attribute;
    }
    if (is_array($attributes) && !empty($attributes)) {
        $this->getConnection()->beginTransaction();
        $data = array_intersect_key($object->getData(), array_flip($attributes));
        try {
            $this->_beforeSaveAttribute($object, $attributes);
            if ($object->getId() && !empty($data)) {
                $this->getConnection()->update(
                    $object->getResource()->getMainTable(),
                    $data,
                    [$object->getResource()->getIdFieldName() . '= ?' => (int)$object->getId()]
                );
                $object->addData($data);
            }
            $this->_afterSaveAttribute($object, $attributes);
            $this->getConnection()->commit();
        } catch (\Exception $e) {
            $this->getConnection()->rollBack();
            throw $e;
        }
    }
    return $this;
}

This way, instead of calling the following:

$this->authorRepository->save($author);

You should be able to do something like this:

$author->getResource()->saveAttribute($author, array_keys($authorData));

As stated in the comments, you'll have to modify that method a bit if you don't need to check the AbstractAttribute instance to match your needs