@@ -35,7 +35,7 @@ return [
     'digits'               => ':attribute 必须是 :digits 位的数字。',
     'digits_between'       => ':attribute 必须是介于 :min 和 :max 位的数字。',
     'dimensions'           => ':attribute 图片尺寸不正确。',
-    'distinct'             => ':attribute 已經存在。',
+    'distinct'             => ':attribute 已经存在。',
     'email'                => ':attribute 不是一个合法的邮箱。',
     'exists'               => ':attribute 不存在。',
     'file'                 => ':attribute 必须是文件。',

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Validation Language Lines
    |--------------------------------------------------------------------------
    |
    | The following language lines contain the default error messages used by
    | the validator class. Some of these rules have multiple versions such
    | such as the size rules. Feel free to tweak each of these messages.
    |
    */

    'accepted'             => ':attribute 必须接受。',
    'active_url'           => ':attribute 不是一个有效的网址。',
    'after'                => ':attribute 必须是一个在 :date 之后的日期。',
    'alpha'                => ':attribute 只能由字母组成。',
    'alpha_dash'           => ':attribute 只能由字母、数字和斜杠组成。',
    'alpha_num'            => ':attribute 只能由字母和数字组成。',
    'array'                => ':attribute 必须是一个数组。',
    'before'               => ':attribute 必须是一个在 :date 之前的日期。',
    'between'              => [
        'numeric' => ':attribute 必须介于 :min - :max 之间。',
        'file'    => ':attribute 必须介于 :min - :max kb 之间。',
        'string'  => ':attribute 必须介于 :min - :max 个字符之间。',
        'array'   => ':attribute 必须只有 :min - :max 个单元。',
    ],
    'boolean'              => ':attribute 必须为布尔值。',
    'confirmed'            => ':attribute 两次输入不一致。',
    'date'                 => ':attribute 不是一个有效的日期。',
    'date_format'          => ':attribute 的格式必须为 :format。',
    'different'            => ':attribute 和 :other 必须不同。',
    'digits'               => ':attribute 必须是 :digits 位的数字。',
    'digits_between'       => ':attribute 必须是介于 :min 和 :max 位的数字。',
    'dimensions'           => ':attribute 图片尺寸不正确。',
    'distinct'             => ':attribute 已經存在。',
    'email'                => ':attribute 不是一个合法的邮箱。',
    'exists'               => ':attribute 不存在。',
    'file'                 => ':attribute 必须是文件。',
    'filled'               => ':attribute 不能为空。',
    'image'                => ':attribute 必须是图片。',
    'in'                   => '已选的属性 :attribute 非法。',
    'in_array'             => ':attribute 没有在 :other 中。',
    'integer'              => ':attribute 必须是整数。',
    'ip'                   => ':attribute 必须是有效的 IP 地址。',
    'json'                 => ':attribute 必须是正确的 JSON 格式。',
    'max'                  => [
        'numeric' => ':attribute 不能大于 :max。',
        'file'    => ':attribute 不能大于 :max kb。',
        'string'  => ':attribute 不能大于 :max 个字符。',
        'array'   => ':attribute 最多只有 :max 个单元。',
    ],
    'mimes'                => ':attribute 必须是一个 :values 类型的文件。',
    'min'                  => [
        'numeric' => ':attribute 必须大于等于 :min。',
        'file'    => ':attribute 大小不能小于 :min kb。',
        'string'  => ':attribute 至少为 :min 个字符。',
        'array'   => ':attribute 至少有 :min 个单元。',
    ],
    'not_in'               => '已选的属性 :attribute 非法。',
    'numeric'              => ':attribute 必须是一个数字。',
    'present'              => ':attribute 必须存在。',
    'regex'                => ':attribute 格式不正确。',
    'required'             => ':attribute 不能为空。',
    'required_if'          => '当 :other 为 :value 时 :attribute 不能为空。',
    'required_unless'      => '当 :other 不为 :value 时 :attribute 不能为空。',
    'required_with'        => '当 :values 存在时 :attribute 不能为空。',
    'required_with_all'    => '当 :values 存在时 :attribute 不能为空。',
    'required_without'     => '当 :values 不存在时 :attribute 不能为空。',
    'required_without_all' => '当 :values 都不存在时 :attribute 不能为空。',
    'same'                 => ':attribute 和 :other 必须相同。',
    'size'                 => [
        'numeric' => ':attribute 大小必须为 :size。',
        'file'    => ':attribute 大小必须为 :size kb。',
        'string'  => ':attribute 必须是 :size 个字符。',
        'array'   => ':attribute 必须为 :size 个单元。',
    ],
    'string'               => ':attribute 必须是一个字符串。',
    'timezone'             => ':attribute 必须是一个合法的时区值。',
    'unique'               => ':attribute 已经存在。',
    'url'                  => ':attribute 格式不正确。',

    /*
    |--------------------------------------------------------------------------
    | Custom Validation Language Lines
    |--------------------------------------------------------------------------
    |
    | Here you may specify custom validation messages for attributes using the
    | convention 'attribute.rule' to name the lines. This makes it quick to
    | specify a specific custom language line for a given attribute rule.
    |
    */

    'custom'               => [
        'attribute-name' => [
            'rule-name' => 'custom-message',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Custom Validation Attributes
    |--------------------------------------------------------------------------
    |
    | The following language lines are used to swap attribute place-holders
    | with something more reader friendly such as E-Mail Address instead
    | of 'email'. This simply helps us make messages a little cleaner.
    |
    */

    'attributes'           => [
        'name'                  => '名称',
        'username'              => '用户名',
        'email'                 => '邮箱',
        'first_name'            => '名',
        'last_name'             => '姓',
        'password'              => '密码',
        'password_confirmation' => '确认密码',
        'city'                  => '城市',
        'country'               => '国家',
        'address'               => '地址',
        'phone'                 => '电话',
        'mobile'                => '手机',
        'age'                   => '年龄',
        'sex'                   => '性别',
        'gender'                => '性别',
        'day'                   => '天',
        'month'                 => '月',
        'year'                  => '年',
        'hour'                  => '时',
        'minute'                => '分',
        'second'                => '秒',
        'title'                 => '标题',
        'content'               => '内容',
        'description'           => '描述',
        'excerpt'               => '摘要',
        'date'                  => '日期',
        'time'                  => '时间',
        'available'             => '可用的',
        'size'                  => '大小',
    ],

];

It should be simplified character. `` -> ``


@@ -64,4 +64,12 @@ class ProductVisibility
     {
         return $this->visible;
     }
+
+    /**
+     * @return \Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup
+     */
+    public function getPricingGroup(): PricingGroup
+    {
+        return $this->pricingGroup;
+    }
 }

<?php

namespace Shopsys\FrameworkBundle\Model\Product;

use Doctrine\ORM\Mapping as ORM;
use Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup;

/**
 * @ORM\Table(name="product_visibilities")
 * @ORM\Entity
 */
class ProductVisibility
{
    /**
     * @var \Shopsys\FrameworkBundle\Model\Product\Product
     *
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Shopsys\FrameworkBundle\Model\Product\Product")
     * @ORM\JoinColumn(nullable=false, name="product_id", referencedColumnName="id", onDelete="CASCADE")
     */
    protected $product;

    /**
     * @var \Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup
     *
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup")
     * @ORM\JoinColumn(nullable=false, name="pricing_group_id", referencedColumnName="id", onDelete="CASCADE")
     */
    protected $pricingGroup;

    /**
     * @var int
     *
     * @ORM\Id
     * @ORM\Column(type="integer")
     */
    protected $domainId;

    /**
     * @var bool
     *
     * @ORM\Column(type="boolean")
     */
    protected $visible;

    /**
     * @param \Shopsys\FrameworkBundle\Model\Product\Product $product
     * @param \Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup $pricingGroup
     * @param int $domainId
     */
    public function __construct(
        Product $product,
        PricingGroup $pricingGroup,
        $domainId
    ) {
        $this->product = $product;
        $this->pricingGroup = $pricingGroup;
        $this->domainId = $domainId;
        $this->visible = false;
    }

    public function isVisible()
    {
        return $this->visible;
    }
}


@@ -14,6 +14,7 @@ use Ergonode\SharedKernel\Domain\AbstractCode;
 class AttributeCode extends AbstractCode
 {
     public const PATTERN = '/^([a-zA-Z0-9_]+)$/';
+    public const FORBIDDEN = ['id'];

     public function __construct(string $value)
     {

<?php

/**
 * Copyright © Bold Brand Commerce Sp. z o.o. All rights reserved.
 * See LICENSE.txt for license details.
 */

declare(strict_types=1);

namespace Ergonode\Attribute\Domain\ValueObject;

use Ergonode\SharedKernel\Domain\AbstractCode;

class AttributeCode extends AbstractCode
{
    public const PATTERN = '/^([a-zA-Z0-9_]+)$/';

    public function __construct(string $value)
    {
        parent::__construct(strtolower($value));
    }

    public static function isValid(string $value): bool
    {
        $value = strtolower($value);

        return parent::isValid($value)
            && preg_match(self::PATTERN, $value);
    }
}


@@ -23,10 +23,11 @@ function roots_scripts() {
   if (is_single() && comments_open() && get_option('thread_comments')) {
     wp_enqueue_script('comment-reply');
   }
-
-  wp_register_script('roots_plugins', get_template_directory_uri() . '/js/plugins.js', false, null, false);
+
+  // Not included by default since this code is used for debugging javas in the console. Uncomment next line and wp_enqueue_script below to use it.
+  //wp_register_script('roots_plugins', get_template_directory_uri() . '/js/plugins.js', false, null, false);
   wp_register_script('roots_main', get_template_directory_uri() . '/js/main.js', false, null, false);
-  wp_enqueue_script('roots_plugins');
+  //wp_enqueue_script('roots_plugins');
   wp_enqueue_script('roots_main');
 }

<?php

function roots_scripts() {
  // Not included by default since Bootstrap's reset supersedes h5bp's. Include if you aren't using Bootstrap.
  //wp_enqueue_style('roots_style', get_template_directory_uri() . '/css/style.css', false, null);
  wp_enqueue_style('roots_bootstrap_style', get_template_directory_uri() . '/css/bootstrap.css', false, null);

  if (BOOTSTRAP_RESPONSIVE) {
    wp_enqueue_style('roots_bootstrap_responsive_style', get_template_directory_uri() . '/css/bootstrap-responsive.css', array('roots_bootstrap_style'), null);
  }

  wp_enqueue_style('roots_app_style', get_template_directory_uri() . '/css/app.css', false, null);

  if (is_child_theme()) {
    wp_enqueue_style('roots_child_style', get_stylesheet_uri());
  }

  if (!is_admin()) {
    wp_deregister_script('jquery');
    wp_register_script('jquery', '', '', '', false);
  }

  if (is_single() && comments_open() && get_option('thread_comments')) {
    wp_enqueue_script('comment-reply');
  }

  wp_register_script('roots_plugins', get_template_directory_uri() . '/js/plugins.js', false, null, false);
  wp_register_script('roots_main', get_template_directory_uri() . '/js/main.js', false, null, false);
  wp_enqueue_script('roots_plugins');
  wp_enqueue_script('roots_main');
}

add_action('wp_enqueue_scripts', 'roots_scripts', 100);

We should still register the script. Just not enqueue it.


@@ -17,6 +17,7 @@
 // along with BOINC.  If not, see <http://www.gnu.org/licenses/>.

 // web interfaces for viewing and controlling batches
+// DEPRECATED: replaced by submit.php

 ini_set('display_errors', 'stdout');
 error_reporting(E_ALL);

<?php
// This file is part of BOINC.
// http://boinc.berkeley.edu
// Copyright (C) 2011 University of California
//
// BOINC is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
//
// BOINC is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with BOINC.  If not, see <http://www.gnu.org/licenses/>.

// web interfaces for viewing and controlling batches

ini_set('display_errors', 'stdout');
error_reporting(E_ALL);

require_once("../inc/util.inc");
require_once("../inc/boinc_db.inc");
require_once("../inc/result.inc");
require_once("../inc/submit_db.inc");

function show_batch($user) {
    $batch_id = get_int('batch_id');
    $batch = BoincBatch::lookup_id($batch_id);
    if (!$batch || $batch->user_id != $user->id) {
        error_page("no batch");
    }
    page_head("Batch $batch->id");
    $results = BoincResult::enum("batch=$batch->id order by workunitid");
    result_table_start(true, true, null);
    foreach ($results as $result) {
        show_result_row($result, true, true, true);
    }
    end_table();
    page_tail();
}

function show_batches($user) {
    $batches = BoincBatch::enum("user_id=$user->id");
    page_head("Batches");
    start_table();
    table_header("Batch ID", "Submitted", "# jobs");
    foreach ($batches as $batch) {
        echo "

            id>$batch->id
            ".time_str($batch->create_time)."
            $batch->njobs

        ";
    }
    end_table();
    page_tail();
}

$user = get_logged_in_user();

$action = get_str('action', true);
switch ($action) {
case '': show_batches($user); break;
case 'show_batch': show_batch($user);
}
?>

This has nothing to do with what is described in the commit message.


@@ -27,7 +27,7 @@ final class CategoryManager implements CategoryManagerInterface
      */
     private $categoryManager;

-    public function __construct(ManagerInterface $categoryManager)
+    public function __construct(?ManagerInterface $categoryManager = null)
     {
         $this->categoryManager = $categoryManager;
     }

<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\MediaBundle\Model;

use Sonata\ClassificationBundle\Model\CategoryInterface;
use Sonata\ClassificationBundle\Model\CategoryManagerInterface as ManagerInterface;

/**
 * @author Joao Albuquerque <albuquerque.joao.filipe@gmail.com>
 * @author Christian Gripp <mail@core23.de>
 */
final class CategoryManager implements CategoryManagerInterface
{
    /**
     * @var ManagerInterface
     */
    private $categoryManager;

    public function __construct(ManagerInterface $categoryManager)
    {
        $this->categoryManager = $categoryManager;
    }

    public function getRootCategory($context): CategoryInterface
    {
        return $this->categoryManager->getRootCategory($context);
    }

    public function getRootCategories($loadChildren = true): iterable
    {
        return $this->categoryManager->getRootCategories($loadChildren);
    }

    public function find($categoryId): ?CategoryInterface
    {
        return $this->categoryManager->find($categoryId);
    }

    public function findBy(array $criteria): iterable
    {
        return $this->categoryManager->findBy($criteria);
    }

    public function findOneBy(array $criteria): ?CategoryInterface
    {
        return $this->categoryManager->findOneBy($criteria);
    }

    public function create(): CategoryInterface
    {
        return $this->categoryManager->create();
    }

    public function save($category): void
    {
        $this->categoryManager->save($category);
    }
}


@@ -62,6 +62,10 @@ class SimpleResizer implements ResizerInterface
         if ($settings['height'] == null) {
             $settings['height'] = (int) ($settings['width'] * $size->getHeight() / $size->getWidth());
         }
+
+        if ($settings['width'] == null) {
+            $settings['width'] = (int) ($settings['height'] * $size->getWidth() / $size->getHeight());
+        }

         return $this->computeBox($media, $settings);
     }

<?php

/*
 * This file is part of the Sonata project.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\MediaBundle\Resizer;

use Imagine\Image\ImagineInterface;
use Imagine\Image\Box;
use Gaufrette\File;
use Sonata\MediaBundle\Model\MediaInterface;
use Imagine\Image\ImageInterface;
use Imagine\Exception\InvalidArgumentException;

class SimpleResizer implements ResizerInterface
{
    protected $adapter;

    protected $mode;

    /**
     * @param \Imagine\Image\ImagineInterface $adapter
     * @param string                          $mode
     */
    public function __construct(ImagineInterface $adapter, $mode)
    {
        $this->adapter = $adapter;
        $this->mode    = $mode;
    }

    /**
     * {@inheritdoc}
     */
    public function resize(MediaInterface $media, File $in, File $out, $format, array $settings)
    {
        if (!isset($settings['width'])) {
            throw new \RuntimeException(sprintf('Width parameter is missing in context "%s" for provider "%s"', $media->getContext(), $media->getProviderName()));
        }

        $image = $this->adapter->load($in->getContent());

        $content = $image
            ->thumbnail($this->getBox($media, $settings), $this->mode)
            ->get($format, array('quality' => $settings['quality']));

        $out->setContent($content);
    }

    /**
     * {@inheritdoc}
     */
    public function getBox(MediaInterface $media, array $settings)
    {
        $size = $media->getBox();

        if ($settings['height'] == null) {
            $settings['height'] = (int) ($settings['width'] * $size->getHeight() / $size->getWidth());
        }

        return $this->computeBox($media, $settings);
    }

    /**
     * @throws \Imagine\Exception\InvalidArgumentException
     *
     * @param \Sonata\MediaBundle\Model\MediaInterface $media
     * @param array                                    $settings
     *
     * @return \Imagine\Image\Box
     */
    private function computeBox(MediaInterface $media, array $settings)
    {
        if ($this->mode !== ImageInterface::THUMBNAIL_INSET && $this->mode !== ImageInterface::THUMBNAIL_OUTBOUND) {
            throw new InvalidArgumentException('Invalid mode specified');
        }

        $size = $media->getBox();

        $ratios = array(
            $settings['width'] / $size->getWidth(),
            $settings['height'] / $size->getHeight()
        );

        if ($this->mode === ImageInterface::THUMBNAIL_INSET) {
            $ratio = min($ratios);
        } else {
            $ratio = max($ratios);
        }

        return $size->scale($ratio);
    }
}

Can you throw an exception if width or height are both null


@@ -2,6 +2,8 @@

 namespace Shopsys\ShopBundle\Command;

+use Shopsys\ShopBundle\Component\Image\DirectoryStructureCreator as ImageDirectoryStructureCreator;
+use Shopsys\ShopBundle\Component\UploadedFile\DirectoryStructureCreator as UploadedFileDirectoryStructureCreator;
 use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;

<?php

namespace Shopsys\ShopBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CreateApplicationDirectoriesCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('shopsys:create-directories')
            ->setDescription('Create application directories for locks, docs, content, images, uploaded files, etc.');
    }

    /**
     * @param \Symfony\Component\Console\Input\InputInterface $input
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $this->createMiscellaneousDirectories($output);
        $this->createImageDirectories($output);
        $this->createUploadedFileDirectories($output);
    }

    private function createMiscellaneousDirectories(OutputInterface $output)
    {
        $rootDirectory = $this->getContainer()->getParameter('shopsys.root_dir');
        $webDirectory = $this->getContainer()->getParameter('shopsys.web_dir');

        $directories = [
            $rootDirectory . '/build/stats',
            $rootDirectory . '/docs/generated',
            $rootDirectory . '/var/cache',
            $rootDirectory . '/var/lock',
            $rootDirectory . '/var/logs',
            $rootDirectory . '/var/errorPages',
            $webDirectory . '/assets/admin/styles',
            $webDirectory . '/assets/frontend/styles',
            $webDirectory . '/assets/scripts',
            $webDirectory . '/content/feeds',
            $webDirectory . '/content/sitemaps',
            $webDirectory . '/content/wysiwyg',
        ];

        $filesystem = $this->getContainer()->get('filesystem');
        /* @var $filesystem \Symfony\Component\Filesystem\Filesystem */

        $filesystem->mkdir($directories);

        $output->writeln('<fg=green>Miscellaneous application directories were successfully created.</fg=green>');
    }

    /**
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     */
    private function createImageDirectories(OutputInterface $output)
    {
        $imageDirectoryStructureCreator = $this->getContainer()
            ->get('shopsys.shop.component.image.directory_structure_creator');
        /* @var $imageDirectoryStructureCreator \Shopsys\ShopBundle\Component\Image\DirectoryStructureCreator */
        $imageDirectoryStructureCreator->makeImageDirectories();

        $output->writeln('<fg=green>Directories for images were successfully created.</fg=green>');
    }

    /**
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     */
    private function createUploadedFileDirectories(OutputInterface $output)
    {
        $uploadedFileDirectoryStructureCreator = $this->getContainer()
            ->get('shopsys.shop.component.uploaded_file.directory_structure_creator');
        /* @var $uploadedFileDirectoryStructureCreator \Shopsys\ShopBundle\Component\UploadedFile\DirectoryStructureCreator */
        $uploadedFileDirectoryStructureCreator->makeUploadedFileDirectories();

        $output->writeln('<fg=green>Directories for UploadedFile entities were successfully created.</fg=green>');
    }
}

From @PetrHeinz review: this should be also aliased (`ImageDirectoryStructureCreator`)


@@ -174,6 +174,7 @@ return [
         'password_confirmation' => 'confirmación de la contraseña',
         'phone'                 => 'teléfono',
         'price'                 => 'precio',
+        'role'                  => 'rol',
         'second'                => 'segundo',
         'sex'                   => 'sexo',
         'subject'               => 'asunto',

<?php

return [
    /*
    |--------------------------------------------------------------------------
    | Validation Language Lines
    |--------------------------------------------------------------------------
    |
    | The following language lines contain the default error messages used by
    | the validator class. Some of these rules have multiple versions such
    | as the size rules. Feel free to tweak each of these messages.
    |
    */

    'accepted'             => ':attribute debe ser aceptado.',
    'active_url'           => ':attribute no es una URL válida.',
    'after'                => ':attribute debe ser una fecha posterior a :date.',
    'after_or_equal'       => ':attribute debe ser una fecha posterior o igual a :date.',
    'alpha'                => ':attribute sólo debe contener letras.',
    'alpha_dash'           => ':attribute sólo debe contener letras, números, guiones y guiones bajos.',
    'alpha_num'            => ':attribute sólo debe contener letras y números.',
    'array'                => ':attribute debe ser un conjunto.',
    'before'               => ':attribute debe ser una fecha anterior a :date.',
    'before_or_equal'      => ':attribute debe ser una fecha anterior o igual a :date.',
    'between'              => [
        'numeric' => ':attribute tiene que estar entre :min - :max.',
        'file'    => ':attribute debe pesar entre :min - :max kilobytes.',
        'string'  => ':attribute tiene que tener entre :min - :max caracteres.',
        'array'   => ':attribute tiene que tener entre :min - :max elementos.',
    ],
    'boolean'              => 'El campo :attribute debe tener un valor verdadero o falso.',
    'confirmed'            => 'La confirmación de :attribute no coincide.',
    'date'                 => ':attribute no es una fecha válida.',
    'date_equals'          => ':attribute debe ser una fecha igual a :date.',
    'date_format'          => ':attribute no corresponde al formato :format.',
    'different'            => ':attribute y :other deben ser diferentes.',
    'digits'               => ':attribute debe tener :digits dígitos.',
    'digits_between'       => ':attribute debe tener entre :min y :max dígitos.',
    'dimensions'           => 'Las dimensiones de la imagen :attribute no son válidas.',
    'distinct'             => 'El campo :attribute contiene un valor duplicado.',
    'email'                => ':attribute no es un correo válido.',
    'ends_with'            => 'El campo :attribute debe finalizar con uno de los siguientes valores: :values',
    'exists'               => ':attribute es inválido.',
    'file'                 => 'El campo :attribute debe ser un archivo.',
    'filled'               => 'El campo :attribute es obligatorio.',
    'gt'                   => [
        'numeric' => 'El campo :attribute debe ser mayor que :value.',
        'file'    => 'El campo :attribute debe tener más de :value kilobytes.',
        'string'  => 'El campo :attribute debe tener más de :value caracteres.',
        'array'   => 'El campo :attribute debe tener más de :value elementos.',
    ],
    'gte'                  => [
        'numeric' => 'El campo :attribute debe ser como mínimo :value.',
        'file'    => 'El campo :attribute debe tener como mínimo :value kilobytes.',
        'string'  => 'El campo :attribute debe tener como mínimo :value caracteres.',
        'array'   => 'El campo :attribute debe tener como mínimo :value elementos.',
    ],
    'image'                => ':attribute debe ser una imagen.',
    'in'                   => ':attribute es inválido.',
    'in_array'             => 'El campo :attribute no existe en :other.',
    'integer'              => ':attribute debe ser un número entero.',
    'ip'                   => ':attribute debe ser una dirección IP válida.',
    'ipv4'                 => ':attribute debe ser una dirección IPv4 válida.',
    'ipv6'                 => ':attribute debe ser una dirección IPv6 válida.',
    'json'                 => 'El campo :attribute debe ser una cadena JSON válida.',
    'lt'                   => [
        'numeric' => 'El campo :attribute debe ser menor que :value.',
        'file'    => 'El campo :attribute debe tener menos de :value kilobytes.',
        'string'  => 'El campo :attribute debe tener menos de :value caracteres.',
        'array'   => 'El campo :attribute debe tener menos de :value elementos.',
    ],
    'lte'                  => [
        'numeric' => 'El campo :attribute debe ser como máximo :value.',
        'file'    => 'El campo :attribute debe tener como máximo :value kilobytes.',
        'string'  => 'El campo :attribute debe tener como máximo :value caracteres.',
        'array'   => 'El campo :attribute debe tener como máximo :value elementos.',
    ],
    'max'                  => [
        'numeric' => ':attribute no debe ser mayor que :max.',
        'file'    => ':attribute no debe ser mayor que :max kilobytes.',
        'string'  => ':attribute no debe ser mayor que :max caracteres.',
        'array'   => ':attribute no debe tener más de :max elementos.',
    ],
    'mimes'                => ':attribute debe ser un archivo con formato: :values.',
    'mimetypes'            => ':attribute debe ser un archivo con formato: :values.',
    'min'                  => [
        'numeric' => 'El tamaño de :attribute debe ser de al menos :min.',
        'file'    => 'El tamaño de :attribute debe ser de al menos :min kilobytes.',
        'string'  => ':attribute debe contener al menos :min caracteres.',
        'array'   => ':attribute debe tener al menos :min elementos.',
    ],
    'multiple_of'          => 'El campo :attribute debe ser múltiplo de :value',
    'not_in'               => ':attribute es inválido.',
    'not_regex'            => 'El formato del campo :attribute no es válido.',
    'numeric'              => ':attribute debe ser numérico.',
    'password'             => 'La contraseña es incorrecta.',
    'present'              => 'El campo :attribute debe estar presente.',
    'regex'                => 'El formato de :attribute es inválido.',
    'required'             => 'El campo :attribute es obligatorio.',
    'required_if'          => 'El campo :attribute es obligatorio cuando :other es :value.',
    'required_unless'      => 'El campo :attribute es obligatorio a menos que :other esté en :values.',
    'required_with'        => 'El campo :attribute es obligatorio cuando :values está presente.',
    'required_with_all'    => 'El campo :attribute es obligatorio cuando :values están presentes.',
    'required_without'     => 'El campo :attribute es obligatorio cuando :values no está presente.',
    'required_without_all' => 'El campo :attribute es obligatorio cuando ninguno de :values está presente.',
    'same'                 => ':attribute y :other deben coincidir.',
    'size'                 => [
        'numeric' => 'El tamaño de :attribute debe ser :size.',
        'file'    => 'El tamaño de :attribute debe ser :size kilobytes.',
        'string'  => ':attribute debe contener :size caracteres.',
        'array'   => ':attribute debe contener :size elementos.',
    ],
    'starts_with'          => 'El campo :attribute debe comenzar con uno de los siguientes valores: :values',
    'string'               => 'El campo :attribute debe ser una cadena de caracteres.',
    'timezone'             => 'El :attribute debe ser una zona válida.',
    'unique'               => 'El campo :attribute ya ha sido registrado.',
    'uploaded'             => 'Subir :attribute ha fallado.',
    'url'                  => 'El formato :attribute es inválido.',
    'uuid'                 => 'El campo :attribute debe ser un UUID válido.',

    /*
    |--------------------------------------------------------------------------
    | Custom Validation Language Lines
    |--------------------------------------------------------------------------
    |
    | Here you may specify custom validation messages for attributes using the
    | convention "attribute.rule" to name the lines. This makes it quick to
    | specify a specific custom language line for a given attribute rule.
    |
    */

    'custom' => [
        'password' => [
            'min' => 'La :attribute debe contener más de :min caracteres',
        ],
        'email'    => [
            'unique' => 'El :attribute ya ha sido registrado.',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Custom Validation Attributes
    |--------------------------------------------------------------------------
    |
    | The following language lines are used to swap attribute place-holders
    | with something more reader friendly such as E-Mail Address instead
    | of "email". This simply helps us make messages a little cleaner.
    |
    */

    'attributes' => [
        'address'               => 'dirección',
        'age'                   => 'edad',
        'body'                  => 'contenido',
        'city'                  => 'ciudad',
        'content'               => 'contenido',
        'country'               => 'país',
        'date'                  => 'fecha',
        'day'                   => 'día',
        'description'           => 'descripción',
        'email'                 => 'correo electrónico',
        'excerpt'               => 'extracto',
        'first_name'            => 'nombre',
        'gender'                => 'género',
        'hour'                  => 'hora',
        'last_name'             => 'apellido',
        'message'               => 'mensaje',
        'minute'                => 'minuto',
        'mobile'                => 'móvil',
        'month'                 => 'mes',
        'name'                  => 'nombre',
        'password'              => 'contraseña',
        'password_confirmation' => 'confirmación de la contraseña',
        'phone'                 => 'teléfono',
        'price'                 => 'precio',
        'second'                => 'segundo',
        'sex'                   => 'sexo',
        'subject'               => 'asunto',
        'terms'                 => 'términos',
        'time'                  => 'hora',
        'title'                 => 'título',
        'username'              => 'usuario',
        'year'                  => 'año',
    ],
];

**ICYMI**: **role** was mentioned in json file.


@@ -11,12 +11,16 @@

 namespace Sonata\MediaBundle\CDN;

+use Symfony\Bundle\FrameworkBundle\Templating\Helper\AssetsHelper;
+
 class Server implements CDNInterface
 {
+    protected $path;
+
     /**
-     * @var string
+     * @var AssetsHelper
      */
-    protected $path;
+    private $assetsHelper;

     /**
      * @param string $path

<?php

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\MediaBundle\CDN;

class Server implements CDNInterface
{
    /**
     * @var string
     */
    protected $path;

    /**
     * @param string $path
     */
    public function __construct($path)
    {
        $this->path = $path;
    }

    /**
     * {@inheritdoc}
     */
    public function getPath($relativePath, $isFlushable)
    {
        return sprintf('%s/%s', rtrim($this->path, '/'), ltrim($relativePath, '/'));
    }

    /**
     * {@inheritdoc}
     */
    public function flushByString($string)
    {
        // nothing to do
    }

    /**
     * {@inheritdoc}
     */
    public function flush($string)
    {
        // nothing to do
    }

    /**
     * {@inheritdoc}
     */
    public function flushPaths(array $paths)
    {
        // nothing to do
    }

    /**
     * {@inheritdoc}
     */
    public function getFlushStatus($identifier)
    {
        // nothing to do
    }
}


<?php

use function Roots\env;

return [

    /*
    |--------------------------------------------------------------------------
    | Application Environment
    |--------------------------------------------------------------------------
    |
    | This value determines the "environment" your application is currently
    | running in. This may determine how you prefer to configure various
    | services your application utilizes. Set this in your ".env" file.
    |
    */

    'env' => env('WP_ENV', 'production'),

    /*
    |--------------------------------------------------------------------------
    | Application Debug Mode
    |--------------------------------------------------------------------------
    |
    | When your application is in debug mode, detailed error messages with
    | stack traces will be shown on every error that occurs within your
    | application. If disabled, a simple generic error page is shown.
    |
    */

    'debug' => env('WP_DEBUG', false),

    /*
    |--------------------------------------------------------------------------
    | Preflight Checks
    |--------------------------------------------------------------------------
    |
    | This value allows service providers to execute preflight tasks after
    | booting. These tasks include creating directories, databases, and files,
    | or doing any other checks to ensure the service is functional.
    |
    */

    'preflight' => false,

    /*
    |--------------------------------------------------------------------------
    | Autoloaded Service Providers
    |--------------------------------------------------------------------------
    |
    | The service providers listed here will be automatically loaded on the
    | request to your application. Feel free to add your own services to
    | this array to grant expanded functionality to your applications.
    |
    */

    'providers' => [
        // App\SomeService\SomeServiceServiceProvider::class,
    ],

    /*
    |--------------------------------------------------------------------------
    | Class Aliases (formerly "Facades")
    |--------------------------------------------------------------------------
    |
    | This array of class aliases will be registered when this application
    | is started. However, feel free to register as many as you wish as
    | the aliases are "lazy" loaded so they don't hinder performance.
    |
    */

    'aliases' => [
        'App' => Illuminate\Support\Facades\App::class,
        'Artisan' => Illuminate\Support\Facades\Artisan::class,
        'Auth' => Illuminate\Support\Facades\Auth::class,
        'Blade' => Illuminate\Support\Facades\Blade::class,
        'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
        'Bus' => Illuminate\Support\Facades\Bus::class,
        'Cache' => Illuminate\Support\Facades\Cache::class,
        'Config' => Illuminate\Support\Facades\Config::class,
        'Cookie' => Illuminate\Support\Facades\Cookie::class,
        'Crypt' => Illuminate\Support\Facades\Crypt::class,
        'DB' => Illuminate\Support\Facades\DB::class,
        'Eloquent' => Illuminate\Database\Eloquent\Model::class,
        'Event' => Illuminate\Support\Facades\Event::class,
        'File' => Illuminate\Support\Facades\File::class,
        'Gate' => Illuminate\Support\Facades\Gate::class,
        'Hash' => Illuminate\Support\Facades\Hash::class,
        'Lang' => Illuminate\Support\Facades\Lang::class,
        'Log' => Illuminate\Support\Facades\Log::class,
        'Mail' => Illuminate\Support\Facades\Mail::class,
        'Notification' => Illuminate\Support\Facades\Notification::class,
        'Password' => Illuminate\Support\Facades\Password::class,
        'Queue' => Illuminate\Support\Facades\Queue::class,
        'Redirect' => Illuminate\Support\Facades\Redirect::class,
        'Redis' => Illuminate\Support\Facades\Redis::class,
        'Request' => Illuminate\Support\Facades\Request::class,
        'Response' => Illuminate\Support\Facades\Response::class,
        'Route' => Illuminate\Support\Facades\Route::class,
        'Schema' => Illuminate\Support\Facades\Schema::class,
        'Session' => Illuminate\Support\Facades\Session::class,
        'Storage' => Illuminate\Support\Facades\Storage::class,
        'URL' => Illuminate\Support\Facades\URL::class,
        'Validator' => Illuminate\Support\Facades\Validator::class,
        'View' => Illuminate\Support\Facades\View::class,
    ],
];

As a compromise, I would say to have preflight set to WP_ENV !== 'production' or something so that by default it's not enabled in production.


@@ -39,8 +39,6 @@ class LanguageTreeReadAction

     /**
-     * @IsGranted("SETTINGS_READ")
-     *
      * @SWG\Tag(name="Language")
      * @SWG\Parameter(
      *     name="language",

<?php
/**
 * Copyright © Bold Brand Commerce Sp. z o.o. All rights reserved.
 * See LICENSE.txt for license details.
 */

declare(strict_types = 1);

namespace Ergonode\Core\Application\Controller\Api\LanguageTree;

use Ergonode\Api\Application\Response\SuccessResponse;
use Ergonode\Core\Domain\Repository\LanguageTreeRepositoryInterface;
use Ergonode\Core\Domain\ValueObject\Language;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Swagger\Annotations as SWG;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route(
 *     path="/tree",
 *     methods={"GET"}
 * )
 */
class LanguageTreeReadAction
{
    /**
     * @var LanguageTreeRepositoryInterface
     */
    private LanguageTreeRepositoryInterface $repository;

    /**
     * @param LanguageTreeRepositoryInterface $repository
     */
    public function __construct(LanguageTreeRepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    /**
     * @IsGranted("SETTINGS_READ")
     *
     * @SWG\Tag(name="Language")
     * @SWG\Parameter(
     *     name="language",
     *     in="path",
     *     type="string",
     *     required=true,
     *     default="en_GB",
     *     description="Language Code",
     * )
     * @SWG\Response(
     *     response=200,
     *     description="Returns Language tree"
     * )
     * @SWG\Response(
     *     response=404,
     *     description="Not found"
     * )
     *
     * @param Language $language
     *
     * @return Response
     */
    public function __invoke(Language $language): Response
    {
        $tree = $this->repository->load();

        return new SuccessResponse($tree);
    }
}

Class import is therefore redundant I guess :)


@@ -129,6 +129,8 @@ class MediaHelper extends Helper
         $provider = $this->getMediaService()
            ->getProvider($media->getProviderName());

+        $format = $provider->getFormatName($media, $format);
+
         return $provider->generatePublicUrl($media, $format);
     }

<?php

/*
 * This file is part of the Sonata project.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace Sonata\MediaBundle\Templating\Helper;

use Symfony\Component\Templating\Helper\Helper;

/**
 * MediaHelper manages action inclusions.
 *
 * @author Thomas Rabaix <thomas.rabaix@sonata-project.com>
 */
class MediaHelper extends Helper
{

    protected $mediaService = null;

    protected $templating = null;

    /**
     * Constructor.
     *
     * @param Constructor $media_provider A MediaProvider instance
     */
    public function __construct($mediaService, $templating)
    {
        $this->mediaService    = $mediaService;
        $this->templating      = $templating;
    }

    /**
     * Returns the canonical name of this helper.
     *
     * @return string The canonical name
     */
    public function getName()
    {
        return 'sonata_media';
    }

    /**
     * Returns the provider view for the provided media
     *
     * @param Media $media
     * @param string $format
     * @param array $options
     * @return string
     */
    public function media($media, $format, $options = array())
    {
        if (!$media) {
            return '';
        }

        $provider = $this
            ->getMediaService()
            ->getProvider($media->getProviderName());

        $format = $provider->getFormatName($media, $format);

        $options = $provider->getHelperProperties($media, $format, $options);

        return $this->getTemplating()->render(
            $provider->getTemplate('helper_view'),
            array(
                 'media'    => $media,
                 'format'   => $format,
                 'options'  => $options,
            )
        );
    }

    /**
     * Returns the thumbnail for the provided media
     *
     * @param Media $media
     * @param string $format
     * @param array $options
     * @return string
     */
    public function thumbnail($media, $format, $options = array())
    {
         if (!$media) {
             return '';
         }

         $provider = $this->getMediaService()
            ->getProvider($media->getProviderName());

         $format = $provider->getFormatName($media, $format);
         $format_definition = $provider->getFormat($format);

         // build option
         $options = array_merge(array(
             'title' => $media->getName(),
             'width' => $format_definition['width'],
         ), $options);

         $options['src'] = $provider->generatePublicUrl($media, $format);

         return $this->getTemplating()->render(
            $provider->getTemplate('helper_thumbnail'),
            array(
                 'media'    => $media,
                 'options'  => $options,
            )
         );
    }

    /**
     * @param Media $media
     * @param string $format
     * @return string
     */
    public function path($media, $format)
    {
        if (!$media) {
             return '';
        }

        $provider = $this->getMediaService()
           ->getProvider($media->getProviderName());

        return $provider->generatePublicUrl($media, $format);
    }

    public function getMediaService()
    {
        return $this->mediaService;
    }

    public function getTemplating()
    {
        return $this->templating;
    }
}

can you provide a dedicated PR for this fix ?


@@ -16,6 +16,11 @@ use Ergonode\BatchAction\Domain\Entity\BatchActionId;
 use Ergonode\SharedKernel\Domain\AggregateId;
 use Symfony\Component\Messenger\Stamp\HandledStamp;
 use Ergonode\BatchAction\Domain\Repository\BatchActionRepositoryInterface;
+use Ergonode\Core\Application\Security\User\CachedUser;
+use Ergonode\Account\Domain\Repository\UserRepositoryInterface;
+use Ergonode\SharedKernel\Domain\Aggregate\UserId;
+use Ergonode\BatchAction\Domain\Event\BatchActionEndedEvent;
+use Ergonode\Core\Application\Messenger\Stamp\UserStamp;

 class BatchActionTransport implements TransportInterface
 {

<?php
/**
 * Copyright © Ergonode Sp. z o.o. All rights reserved.
 * See LICENSE.txt for license details.
 */

declare(strict_types=1);

namespace Ergonode\BatchAction\Application\Transport;

use Symfony\Component\Messenger\Transport\TransportInterface;
use Doctrine\DBAL\Connection;
use Symfony\Component\Messenger\Envelope;
use Ergonode\BatchAction\Domain\Command\ProcessBatchActionEntryCommand;
use Ergonode\BatchAction\Domain\Entity\BatchActionId;
use Ergonode\SharedKernel\Domain\AggregateId;
use Symfony\Component\Messenger\Stamp\HandledStamp;
use Ergonode\BatchAction\Domain\Repository\BatchActionRepositoryInterface;

class BatchActionTransport implements TransportInterface
{
    private Connection $connection;

    private BatchActionRepositoryInterface $repository;

    public function __construct(Connection $connection, BatchActionRepositoryInterface $repository)
    {
        $this->connection = $connection;
        $this->repository = $repository;
    }

    public function get(): iterable
    {
        $this->connection->beginTransaction();
        $result = [];
        $record = $this->connection->executeQuery(
            'SELECT batch_action_id, resource_id
                 FROM batch_action_entry
                 WHERE processed_at is NULL LIMIT 1 FOR UPDATE SKIP LOCKED'
        )->fetchAssociative();

        if (!empty($record)) {
            echo ($record['resource_id']).PHP_EOL;
            $result[] = new Envelope(
                new ProcessBatchActionEntryCommand(
                    new BatchActionId($record['batch_action_id']),
                    new AggregateId($record['resource_id'])
                )
            );
        } else {
            $this->connection->commit();
        }

        return $result;
    }

    public function ack(Envelope $envelope): void
    {
        /** @var ProcessBatchActionEntryCommand $message */
        $message = $envelope->getMessage();
        /** @var HandledStamp $stamp */
        $stamp = $envelope->last(HandledStamp::class);

        $this->repository->markEntry($message->getId(), $message->getResourceId(), $stamp->getResult());
        $this->connection->commit();
    }

    public function reject(Envelope $envelope): void
    {
        $this->connection->commit();
    }

    public function send(Envelope $envelope): Envelope
    {
        return $envelope;
    }
}

As far as I can see you're not rollbacking transaction anywhere. This could potentially cause further issues(and very hard to debug ones) with not synchronized transactions between message handling. Is the transaction required in this place anyway? You seem only to be fetching data here. ack method is performing some updates so it makes sense


@@ -3,6 +3,7 @@
 namespace Shopsys\FrameworkBundle\Twig;

 use Shopsys\FrameworkBundle\Component\Domain\Domain;
+use Shopsys\FrameworkBundle\Component\UploadedFile\UploadedFile;
 use Shopsys\FrameworkBundle\Component\UploadedFile\UploadedFileFacade;
 use Shopsys\FrameworkBundle\Twig\FileThumbnail\FileThumbnailExtension;
 use Twig_Extension;

<?php

namespace Shopsys\FrameworkBundle\Twig;

use Shopsys\FrameworkBundle\Component\Domain\Domain;
use Shopsys\FrameworkBundle\Component\UploadedFile\UploadedFileFacade;
use Shopsys\FrameworkBundle\Twig\FileThumbnail\FileThumbnailExtension;
use Twig_Extension;
use Twig_SimpleFunction;

class UploadedFileExtension extends Twig_Extension
{
    /**
     * @var \Shopsys\FrameworkBundle\Component\Domain\Domain
     */
    protected $domain;

    /**
     * @var \Shopsys\FrameworkBundle\Component\UploadedFile\UploadedFileFacade
     */
    protected $uploadedFileFacade;

    /**
     * @var \Shopsys\FrameworkBundle\Twig\FileThumbnail\FileThumbnailExtension
     */
    protected $fileThumbnailExtension;

    /**
     * @param \Shopsys\FrameworkBundle\Component\Domain\Domain $domain
     * @param \Shopsys\FrameworkBundle\Component\UploadedFile\UploadedFileFacade $uploadedFileFacade
     * @param \Shopsys\FrameworkBundle\Twig\FileThumbnail\FileThumbnailExtension $fileThumbnailExtension
     */
    public function __construct(
        Domain $domain,
        UploadedFileFacade $uploadedFileFacade,
        FileThumbnailExtension $fileThumbnailExtension
    ) {
        $this->domain = $domain;
        $this->uploadedFileFacade = $uploadedFileFacade;
        $this->fileThumbnailExtension = $fileThumbnailExtension;
    }

    /**
     * @return array
     */
    public function getFunctions()
    {
        return [
            new Twig_SimpleFunction('hasUploadedFile', [$this, 'hasUploadedFile']),
            new Twig_SimpleFunction('uploadedFileUrl', [$this, 'getUploadedFileUrl']),
            new Twig_SimpleFunction('uploadedFilePreview', [$this, 'getUploadedFilePreviewHtml'], ['is_safe' => ['html']]),
            new Twig_SimpleFunction('getUploadedFile', [$this, 'getUploadedFileByEntity']),
        ];
    }

    /**
     * @param Object $entity
     * @return bool
     */
    public function hasUploadedFile($entity)
    {
        return $this->uploadedFileFacade->hasUploadedFile($entity);
    }

    /**
     * @param Object $entity
     * @return string
     */
    public function getUploadedFileUrl($entity)
    {
        $uploadedFile = $this->getUploadedFileByEntity($entity);

        return $this->uploadedFileFacade->getUploadedFileUrl($this->domain->getCurrentDomainConfig(), $uploadedFile);
    }

    /**
     * @param Object $entity
     * @return string
     */
    public function getUploadedFilePreviewHtml($entity)
    {
        $uploadedFile = $this->getUploadedFileByEntity($entity);
        $filepath = $this->uploadedFileFacade->getAbsoluteUploadedFileFilepath($uploadedFile);
        $fileThumbnailInfo = $this->fileThumbnailExtension->getFileThumbnailInfo($filepath);

        if ($fileThumbnailInfo->getIconType() !== null) {
            $classes = [
                'svg',
                'svg-file-' . $fileThumbnailInfo->getIconType(),
                'list-images__item__image__type',
                'list-images__item__image__type--' . $fileThumbnailInfo->getIconType(),
                'text-no-decoration',
                'cursor-pointer',
            ];

            return '';
        } else {
            return 'getImageUri() . '"/>';
        }
    }

    /**
     * @param Object $entity
     * @return \Shopsys\FrameworkBundle\Component\UploadedFile\UploadedFile
     */
    public function getUploadedFileByEntity($entity)
    {
        return $this->uploadedFileFacade->getUploadedFileByEntity($entity);
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'file_extension';
    }
}

I can imagine to define strict type for this extension as well. It is part of file upload refactoring


@@ -12,9 +12,21 @@

 namespace HookAnalytics;

+use Propel\Runtime\Connection\ConnectionInterface;
+use Thelia\Model\ConfigQuery;
+use Thelia\Model\LangQuery;
 use Thelia\Module\BaseModule;

 class HookAnalytics extends BaseModule
 {
-
+    public function update($currentVersion, $newVersion, ConnectionInterface $con = null)
+    {
+        if (version_compare($newVersion, $currentVersion, ">")){
+            $langs = LangQuery::create()->filterByActive()->find();
+            $config = ConfigQuery::read("hookanalytics_trackingcode", "");
+            foreach ($langs as $lang){
+                self::setConfigValue('hookanalytics_trackingcode', $config, $lang->getLocale());
+            }
+        }
+    }
 }

<?php
/*************************************************************************************/
/*      This file is part of the Thelia package.                                     */
/*                                                                                */
/*      Copyright (c) OpenStudio                                                     */
/*      email : dev@thelia.net                                                       */
/*      web : http://www.thelia.net                                                  */
/*                                                                                */
/*      For the full copyright and license information, please view the LICENSE.txt  */
/*      file that was distributed with this source code.                             */
/*************************************************************************************/

namespace HookAnalytics;

use Thelia\Module\BaseModule;

class HookAnalytics extends BaseModule
{

}

This will copy the trackingcode at each update even for all future updates and erase filed values. You should check that the current version is less or equal to 2.4.3 and the new version greater than 2.4.3


@@ -33,7 +33,7 @@ class ProfileController extends BaseController
         $user = $this->getUser();

         return $this->renderTemplate('users/edit.html.twig', [
-            'usertitle' => $user->getFullName(),
+            'usertitle' => $user->getDisplayName(),
             'user' => $user,
         ]);
     }

<?php

declare(strict_types=1);

namespace Bolt\Controller\Backend;

use Bolt\Controller\BaseController;
use Doctrine\Common\Persistence\ObjectManager;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

/**
 * Class ProfileController.
 *
 * @Security("has_role('ROLE_ADMIN')")
 */
class ProfileController extends BaseController
{
    /**
     * @Route("/profile-edit", methods={"GET"}, name="bolt_profile_edit")
     *
     * @throws \Twig_Error_Loader
     * @throws \Twig_Error_Runtime
     * @throws \Twig_Error_Syntax
     */
    public function profileEdit(Request $request): Response
    {
        $user = $this->getUser();

        return $this->renderTemplate('users/edit.html.twig', [
            'usertitle' => $user->getFullName(),
            'user' => $user,
        ]);
    }

    /**
     * @Route("/profile-edit", methods={"POST"}, name="bolt_profile_edit_post")
     *
     * @throws \Twig_Error_Loader
     * @throws \Twig_Error_Runtime
     * @throws \Twig_Error_Syntax
     */
    public function profileEditPost(Request $request, UrlGeneratorInterface $urlGenerator, ObjectManager $manager, UserPasswordEncoderInterface $encoder): Response
    {
        $user = $this->getUser();
        $userTitle = $user->getFullName();
        $url = $urlGenerator->generate('bolt_profile_edit');
        $locale = current($request->get('locale'));
        $newPassword = $request->get('password');

        $user->setFullName($request->get('fullName'));
        $user->setEmail($request->get('email'));
        $user->setLocale($locale);
        $user->setbackendTheme($request->get('user')['backendTheme']);

        $hasError = false;

        $usernameValidateOptions = [
            'options' => [
                'min_range' => 1,
            ],
        ];

        // Validate username
        if (! filter_var(mb_strlen($user->getFullName()), FILTER_VALIDATE_INT, $usernameValidateOptions)) {
            $this->addFlash('danger', 'user.not_valid_username');
            $hasError = true;
        }

        // Validate password
        if (! empty($newPassword) && mb_strlen($newPassword) < 6) {
            $this->addFlash('danger', 'user.not_valid_password');
            $hasError = true;
        } elseif (! empty($newPassword) && mb_strlen($newPassword) > 6) {
            $user->setPassword($encoder->encodePassword($user, $newPassword));
        }

        // Validate email
        if (! filter_var($user->getEmail(), FILTER_VALIDATE_EMAIL)) {
            $this->addFlash('danger', 'user.not_valid_email');
            $hasError = true;
        }

        if ($hasError) {
            return $this->renderTemplate('users/edit.html.twig', [
                'usertitle' => $userTitle,
                'user' => $user,
            ]);
        }

        $manager->flush();

        $request->getSession()->set('_locale', $locale);

        return new RedirectResponse($url);
    }
}


@@ -34,7 +34,9 @@ class TCallableObject extends TObject
         return false;
     }

-    public function getAssertionString(bool $exact = false): string
+    public function getAssertionString()
+
+    /** @psalm-mutation-free */: string
     {
         return 'object';
     }

<?php

namespace Psalm\Type\Atomic;

/**
 * Denotes an object that is also `callable` (i.e. it has `__invoke` defined).
 */
class TCallableObject extends TObject
{
    public function __toString(): string
    {
        return 'callable-object';
    }

    public function getKey(bool $include_extra = true): string
    {
        return 'callable-object';
    }

    /**
     * @param  array<lowercase-string, string> $aliased_classes
     */
    public function toPhpString(
        ?string $namespace,
        array $aliased_classes,
        ?string $this_class,
        int $analysis_php_version_id
    ): ?string {
        return $analysis_php_version_id >= 7_02_00 ? 'object' : null;
    }

    public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool
    {
        return false;
    }

    public function getAssertionString(bool $exact = false): string
    {
        return 'object';
    }
}


@@ -50,7 +50,7 @@ class MediaType extends AbstractType
             'new_on_update' => $options['new_on_update'],
         )));

-        $builder->addEventListener(FormEvents::BIND, function (FormEvent $event) {
+        $builder->addEventListener(FormEvents::SUBMIT, function(FormEvent $event) {
             if ($event->getForm()->has('unlink') && $event->getForm()->get('unlink')->getData()) {
                 $event->setData(null);
             }

<?php

/*
 * This file is part of the Sonata project.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\MediaBundle\Form\Type;

use Sonata\MediaBundle\Form\DataTransformer\ProviderDataTransformer;
use Sonata\MediaBundle\Provider\Pool;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class MediaType extends AbstractType
{
    protected $pool;

    protected $class;

    /**
     * @param Pool   $pool
     * @param string $class
     */
    public function __construct(Pool $pool, $class)
    {
        $this->pool  = $pool;
        $this->class = $class;
    }

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->addModelTransformer(new ProviderDataTransformer($this->pool, $this->class, array(
            'provider'      => $options['provider'],
            'context'       => $options['context'],
            'empty_on_new'  => $options['empty_on_new'],
            'new_on_update' => $options['new_on_update'],
        )));

        $builder->addEventListener(FormEvents::BIND, function (FormEvent $event) {
            if ($event->getForm()->has('unlink') && $event->getForm()->get('unlink')->getData()) {
                $event->setData(null);
            }
        });

        $this->pool->getProvider($options['provider'])->buildMediaType($builder);

        $builder->add('unlink', 'checkbox', array(
            'mapped'   => false,
            'data'     => false,
            'required' => false,
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        $view->vars['provider'] = $options['provider'];
        $view->vars['context'] = $options['context'];
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Remove it when bumping requirements to Symfony >=2.7
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $this->configureOptions($resolver);
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class'    => $this->class,
            'provider'      => null,
            'context'       => null,
            'empty_on_new'  => true,
            'new_on_update' => true,
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getParent()
    {
        return 'form';
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'sonata_media_type';
    }
}

`function (` is correct. A space must be there. Please run `make cs` to have correct coding style.


@@ -41,6 +41,6 @@ class EntityFilterConfigurator implements TypeConfiguratorInterface
      */
     public function supports($type, array $options, array $metadata): bool
     {
-        return EntityFilter::class === $metadata['type'] && 'association' === $metadata['dataType'] ?? null;
+        return (EntityFilter::class === $metadata['type']) && ('association' === $metadata['dataType'] ?? null);
     }
 }

<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Form\Type\Configurator;

use Doctrine\ORM\Mapping\ClassMetadata;
use EasyCorp\Bundle\EasyAdminBundle\Form\Filter\EntityFilter;
use Symfony\Component\Form\FormConfigInterface;

/**
 * @author Yonel Ceruto <yonelceruto@gmail.com>
 */
class EntityFilterConfigurator implements TypeConfiguratorInterface
{
    /**
     * {@inheritdoc}
     */
    public function configure($name, array $options, array $metadata, FormConfigInterface $parentConfig): array
    {
        $entityTypeOptions = $options['value_type_options'] ?? [];

        if (!isset($entityTypeOptions['class'])) {
            $entityTypeOptions['class'] = $metadata['targetEntity'];
        }

        if (!isset($entityTypeOptions['multiple']) && $metadata['associationType'] & ClassMetadata::TO_MANY) {
            $entityTypeOptions['multiple'] = true;
        } elseif (($metadata['associationType'] & ClassMetadata::TO_ONE) && !isset($options['placeholder']) && (!isset($options['required']) || false === $options['required'])) {
            $entityTypeOptions['placeholder'] = 'label.form.empty_value';
        }

        // Supported associations are displayed using advanced JavaScript widgets
        $entityTypeOptions['attr']['data-widget'] = 'select2';

        $options['value_type_options'] = $entityTypeOptions;

        return $options;
    }

    /**
     * {@inheritdoc}
     */
    public function supports($type, array $options, array $metadata): bool
    {
        return EntityFilter::class === $metadata['type'] && 'association' === $metadata['dataType'] ?? null;
    }
}


@@ -34,7 +34,7 @@ class MediaAdmin extends Admin
      */
     public function createQuery($context = 'list')
     {
-        $query = $this->getModelManager()->createQuery($this->getClass(), '', $this->root);
+        $query = $this->getModelManager()->createQuery($this->getClass(), 'a', $this->root);

         foreach ($this->extensions as $extension) {
             $extension->configureQuery($this, $query, $context);

<?php

/*
 * This file is part of the Sonata package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\MediaBundle\Admin\PHPCR;

use Sonata\MediaBundle\Admin\BaseMediaAdmin as Admin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Route\RouteCollection;

class MediaAdmin extends Admin
{
    /**
     * Path to the root node of media documents.
     *
     * @var string
     */
    protected $root;

    public function setRoot($root)
    {
        $this->root = $root;
    }

    /**
     * {@inheritdoc}
     */
    public function createQuery($context = 'list')
    {
        $query = $this->getModelManager()->createQuery($this->getClass(), '', $this->root);

        foreach ($this->extensions as $extension) {
            $extension->configureQuery($this, $query, $context);
        }

        return $query;
    }

    public function id($object)
    {
        return $this->getUrlsafeIdentifier($object);
    }

    /**
     * @param  \Sonata\AdminBundle\Datagrid\DatagridMapper $datagridMapper
     * @return void
     */
    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        // TODO disabled filter due to no attached service for filter types: string, checkbox
//        $datagridMapper
//            ->add('name')
//            ->add('providerReference')
//            ->add('enabled')
//            ->add('context')
//        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function configureRoutes(RouteCollection $collection)
    {
        // Allow path in id parameter
        $collection->add('view', $this->getRouterIdParameter() . '/view', array(), array('id' => '.+', '_method' => 'GET'));
        $collection->add('show', $this->getRouterIdParameter() . '/show', array(
                '_controller' => sprintf('%s:%s', $this->baseControllerName, 'view')
            ),
            array('id' => '.+', '_method' => 'GET')
        );
    }
}

PHPCR-ODM does not allow blank aliases..


@@ -4,35 +4,46 @@ declare(strict_types=1);

 namespace Tests\ShopBundle\Functional\Model\Payment;

-use Shopsys\FrameworkBundle\Model\Payment\PaymentDataFactoryInterface;
 use Shopsys\FrameworkBundle\Model\Pricing\Vat\Vat;
 use Shopsys\FrameworkBundle\Model\Pricing\Vat\VatData;
-use Shopsys\FrameworkBundle\Model\Transport\TransportDataFactoryInterface;
-use Shopsys\FrameworkBundle\Model\Transport\TransportFacade;
 use Shopsys\ShopBundle\Model\Payment\Payment;
 use Shopsys\ShopBundle\Model\Transport\Transport;
 use Tests\ShopBundle\Test\TransactionFunctionalTestCase;

 class PaymentTest extends TransactionFunctionalTestCase
 {
+    /**
+     * @var \Shopsys\ShopBundle\Model\Payment\PaymentDataFactory
+     * @inject
+     */
+    private $paymentDataFactory;
+
+    /**
+     * @var \Shopsys\ShopBundle\Model\Transport\TransportDataFactory
+     * @inject
+     */
+    private $transportDataFactory;
+
+    /**
+     * @var \Shopsys\FrameworkBundle\Model\Transport\TransportFacade
+     * @inject
+     */
+    private $transportFacade;
+
     public function testRemoveTransportFromPaymentAfterDelete()
     {
-        /** @var \Shopsys\ShopBundle\Model\Payment\PaymentDataFactory $paymentDataFactory */
-        $paymentDataFactory = $this->getContainer()->get(PaymentDataFactoryInterface::class);
-        /** @var \Shopsys\ShopBundle\Model\Transport\TransportDataFactory $transportDataFactory */
-        $transportDataFactory = $this->getContainer()->get(TransportDataFactoryInterface::class);
         $em = $this->getEntityManager();

         $vatData = new VatData();
         $vatData->name = 'vat';
         $vatData->percent = '21';
         $vat = new Vat($vatData);
-        $transportData = $transportDataFactory->create();
+        $transportData = $this->transportDataFactory->create();
         $transportData->name['cs'] = 'name';
         $transportData->vat = $vat;
         $transport = new Transport($transportData);

-        $paymentData = $paymentDataFactory->create();
+        $paymentData = $this->paymentDataFactory->create();
         $paymentData->name['cs'] = 'name';
         $paymentData->vat = $vat;

<?php

declare(strict_types=1);

namespace Tests\ShopBundle\Functional\Model\Payment;

use Shopsys\FrameworkBundle\Model\Payment\PaymentDataFactoryInterface;
use Shopsys\FrameworkBundle\Model\Pricing\Vat\Vat;
use Shopsys\FrameworkBundle\Model\Pricing\Vat\VatData;
use Shopsys\FrameworkBundle\Model\Transport\TransportDataFactoryInterface;
use Shopsys\FrameworkBundle\Model\Transport\TransportFacade;
use Shopsys\ShopBundle\Model\Payment\Payment;
use Shopsys\ShopBundle\Model\Transport\Transport;
use Tests\ShopBundle\Test\TransactionFunctionalTestCase;

class PaymentTest extends TransactionFunctionalTestCase
{
    public function testRemoveTransportFromPaymentAfterDelete()
    {
        /** @var \Shopsys\ShopBundle\Model\Payment\PaymentDataFactory $paymentDataFactory */
        $paymentDataFactory = $this->getContainer()->get(PaymentDataFactoryInterface::class);
        /** @var \Shopsys\ShopBundle\Model\Transport\TransportDataFactory $transportDataFactory */
        $transportDataFactory = $this->getContainer()->get(TransportDataFactoryInterface::class);
        $em = $this->getEntityManager();

        $vatData = new VatData();
        $vatData->name = 'vat';
        $vatData->percent = '21';
        $vat = new Vat($vatData);
        $transportData = $transportDataFactory->create();
        $transportData->name['cs'] = 'name';
        $transportData->vat = $vat;
        $transport = new Transport($transportData);

        $paymentData = $paymentDataFactory->create();
        $paymentData->name['cs'] = 'name';
        $paymentData->vat = $vat;

        $payment = new Payment($paymentData);
        $payment->addTransport($transport);

        $em->persist($vat);
        $em->persist($transport);
        $em->persist($payment);
        $em->flush();

        /** @var \Shopsys\FrameworkBundle\Model\Transport\TransportFacade $transportFacade */
        $transportFacade = $this->getContainer()->get(TransportFacade::class);
        $transportFacade->deleteById($transport->getId());

        $this->assertNotContains($transport, $payment->getTransports());
    }
}

Hmm, this is not purpose of this PR, but we should create *Data objects by *DataFactory classes. Why? When someone overrided VatData class, this test can fail. The same is true for entities for a few lines below. And in other tests.


@@ -54,7 +54,7 @@ class DbalBatchActionQuery implements BatchActionQueryInterface
             $record['all'],
             $record['processed'],
             new \DateTime($record['created_at']),
-            $record['all'] === $record['processed'] ? new \DateTime($record['last_processed_at']) : null
+            $record['all'] === $record['processed'] ? new \DateTime($record['last_processed_at'] ?? $record['created_at']) : null
         );

         foreach ($this->getEntries($id, $language) as $entry) {

<?php
/**
 * Copyright © Bold Brand Commerce Sp. z o.o. All rights reserved.
 * See LICENSE.txt for license details.
 */

declare(strict_types=1);

namespace Ergonode\BatchAction\Infrastructure\Persistence\Query;

use Ergonode\BatchAction\Domain\Entity\BatchActionId;
use Ergonode\BatchAction\Domain\Model\BatchActionInformationModel;
use Ergonode\BatchAction\Domain\Query\BatchActionQueryInterface;
use Doctrine\DBAL\Connection;
use Ergonode\BatchAction\Domain\ValueObject\BatchActionType;
use Symfony\Contracts\Translation\TranslatorInterface;
use Ergonode\Core\Domain\ValueObject\Language;
use Ergonode\SharedKernel\Domain\AggregateId;
use Ergonode\BatchAction\Domain\Model\BatchActionEntryModel;

class DbalBatchActionQuery implements BatchActionQueryInterface
{
    private Connection $connection;

    private TranslatorInterface $translator;

    public function __construct(Connection $connection, TranslatorInterface $translator)
    {
        $this->connection = $connection;
        $this->translator = $translator;
    }

    public function getInformation(BatchActionId $id, Language $language): BatchActionInformationModel
    {
        $qb = $this->connection->createQueryBuilder();

        $record = $qb
            ->select('id, created_at, type')
            ->addSelect('(SELECT count(*) FROM batch_action_entry WHERE batch_action_id = id) AS all')
            ->addSelect('(SELECT count(*) FROM batch_action_entry WHERE batch_action_id = id
            AND success IS NOT NULL) AS processed')
            ->addSelect('(SELECT max(processed_at) FROM batch_action_entry WHERE batch_action_id = id)
            AS last_processed_at')
            ->from('batch_action')
            ->where($qb->expr()->eq('id', ':id'))
            ->setParameter(':id', $id->getValue())
            ->execute()
            ->fetch();

        $model = new BatchActionInformationModel(
            new BatchActionId($record['id']),
            new BatchActionType($record['type']),
            $record['all'],
            $record['processed'],
            new \DateTime($record['created_at']),
            $record['all'] === $record['processed'] ? new \DateTime($record['last_processed_at']) : null
        );

        foreach ($this->getEntries($id, $language) as $entry) {
            $model->addEntry($entry);
        }

        return $model;
    }

    private function getEntries(BatchActionId $id, Language $language): array
    {
        $result = [];
        $qb = $this->connection->createQueryBuilder();
        $records = $qb
            ->select('fail_reason, resource_id')
            ->from('batch_action_entry')
            ->where($qb->expr()->eq('batch_action_id', ':id'))
            ->setParameter(':id', $id->getValue())
            ->andWhere($qb->expr()->eq('success', ':success'))
            ->setParameter(':success', false, \PDO::PARAM_BOOL)
            ->execute()
            ->fetchAll();

        foreach ($records as $record) {
            $entry = new BatchActionEntryModel(new AggregateId($record['resource_id']));
            $messages = json_decode($record['fail_reason'], true, 512, JSON_THROW_ON_ERROR);
            foreach ($messages as $message) {
                $entry->addMessage($this->translate($message, $language));
            }
            $result[] = $entry;
        }

        return $result;
    }

    private function translate(array $record, Language $language): string
    {
        return $this->translator->trans(
            $record['message'],
            $record['properties'],
            'batch-action',
            $language->getCode()
        );
    }
}


@@ -13,12 +13,14 @@ use RootedData\Exception\ValidationException;
  * Json Response Trait.
  */
 trait JsonResponseTrait {
+  use CacheableResponseTrait;

   /**
    * Private.
    */
   private function getResponse($message, int $code = 200): JsonResponse {
-    return new JsonResponse($message, $code, []);
+    $response = new JsonResponse($message, $code, []);
+    return $this->addCacheHeaders($response);
   }

   /**

<?php

namespace Drupal\common;

use Symfony\Component\HttpFoundation\JsonResponse;
use OpisErrorPresenter\Implementation\MessageFormatterFactory;
use OpisErrorPresenter\Implementation\PresentedValidationErrorFactory;
use OpisErrorPresenter\Implementation\Strategies\BestMatchError;
use OpisErrorPresenter\Implementation\ValidationErrorPresenter;
use RootedData\Exception\ValidationException;

/**
 * Json Response Trait.
 */
trait JsonResponseTrait {

  /**
   * Private.
   */
  private function getResponse($message, int $code = 200): JsonResponse {
    return new JsonResponse($message, $code, []);
  }

  /**
   * Create JSON response from a caught exception.
   *
   * @param \Exception $e
   *   Exception object.
   * @param int $code
   *   HTTP response code.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   A Symfony JSON response.
   */
  private function getResponseFromException(\Exception $e, int $code = 400):JsonResponse {
    $body = [
      'message' => $e->getMessage(),
      'status' => $code,
      "timestamp" => date("c"),
    ];
    if ($data = $this->getExceptionData($e)) {
      $body['data'] = $data;
    }
    return $this->getResponse((object) $body, $code);
  }

  /**
   * See if we can present more detail about the exception.
   *
   * Currently, only RootedJsonData validation errors supported.
   *
   * @param \Exception $e
   *   Exception object.
   *
   * @return array|false
   *   An array of data to explain the errors.
   */
  private function getExceptionData(\Exception $e) {
    if ($e instanceof ValidationException) {
      $errors = $e->getResult()->getErrors();
      $presenter = new ValidationErrorPresenter(
        new PresentedValidationErrorFactory(
          new MessageFormatterFactory()
        ),
        new BestMatchError()
      );
      $presented = $presenter->present(...$errors);
      return $presented[0];
    }

    return FALSE;
  }

}

I would say we should not use the `CacheableResponseTrait` within the `JsonResponseTrait`. Traits within traits tend to lead to a bad developer experience as it can be really hard to find the actual method you're seeing in the implementing class, and in this case it looks like we're using _both_ the cacheable and the JSON traits in the same controller, so it's redundant anyway. I'd recommend just removing that use statement from `JsonResponseTrait` and make sure `ChaceableResponseTrait` is included everywhere it's needed specifically.


@@ -29,6 +29,14 @@ class SkuTest extends TestCase
         $this->assertEquals($sku, $sku->getValue());
     }

+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testInvalidVAlue(): void
+    {
+        $sku = new Sku('yKL3rWXluEM7NapnMSVpYHpicQJkpJ0Obfx91mma0xwnQxUfsrwy5Nfki1LUZR4qYolBTlUFDO4RkeINIkjPzMfTSit0bQZJevXA6GMsj0LnSZaiT1bBfr00vtKWqLAPollonRzb6GBVlT5U9I6nC49r3Vnj2jUgpA73CvfnVnFBNnHqCaI2Cu48SKaVSRGgROhoD1dGPvvq98okavZ3ktVHk0TcyyiyfoH52U3gP3J5bNTVZngivjPJAqtOW8TO');
+    }
+
     /**
      * @return array
      */

<?php

/**
 * Copyright © Bold Brand Commerce Sp. z o.o. All rights reserved.
 * See LICENSE.txt for license details.
 */

declare(strict_types = 1);

namespace Ergonode\Product\Tests\Domain\ValueObject;

use Ergonode\Product\Domain\ValueObject\Sku;
use PHPUnit\Framework\TestCase;

/**
 * Class SkuTest
 */
class SkuTest extends TestCase
{
    /**
     * @dataProvider data
     *
     * @param string $sku
     */
    public function testGetValue(string $sku): void
    {
        $sku = new Sku($sku);

        $this->assertEquals($sku, $sku->getValue());
    }

    /**
     * @return array
     */
    public function data(): array
    {
        return [
            ['B.AD46SB/BN926'],
        ];
    }
}

This entry will be incompatible with the currently implemented formatting rules. Maximum 120 characters per line.


@@ -18,7 +18,11 @@ use Symfony\Component\Validator\Constraints\NotBlank;
 use Symfony\Component\Validator\Constraints\NotEqualTo;
 use Symfony\Component\Validator\ExecutionContextInterface;
 use Thelia\Core\Translation\Translator;
+use Thelia\Model\Base\CountryQuery;
 use Thelia\Model\Base\LangQuery;
+use Thelia\Model\Base\ModuleQuery;
+use Thelia\Model\Module;
+use Thelia\Module\BaseModule;

 /**
  * Allow to build a form Coupon

<?php
/*************************************************************************************/
/*      This file is part of the Thelia package.                                     */
/*                                                                                */
/*      Copyright (c) OpenStudio                                                     */
/*      email : dev@thelia.net                                                       */
/*      web : http://www.thelia.net                                                  */
/*                                                                                */
/*      For the full copyright and license information, please view the LICENSE.txt  */
/*      file that was distributed with this source code.                             */
/*************************************************************************************/

namespace Thelia\Form;

use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotEqualTo;
use Symfony\Component\Validator\ExecutionContextInterface;
use Thelia\Core\Translation\Translator;
use Thelia\Model\Base\LangQuery;

/**
 * Allow to build a form Coupon
 *
 * @package Coupon
 * @author  Guillaume MOREL <gmorel@openstudio.fr>
 *
 */
class CouponCreationForm extends BaseForm
{
    /**
     * Build Coupon form
     *
     * @return void
     */
    protected function buildForm()
    {
        $this->formBuilder
            ->add(
                'code',
                'text',
                array(
                    'constraints' => array(
                        new NotBlank()
                    )
                )
            )
            ->add(
                'title',
                'text',
                array(
                    'constraints' => array(
                        new NotBlank()
                    )
                )
            )
            ->add(
                'shortDescription',
                'text'

            )
            ->add(
                'description',
                'textarea'

            )
            ->add(
                'type',
                'text',
                array(
                    'constraints' => array(
                        new NotBlank(),
                        new NotEqualTo(
                            array(
                                'value' => -1
                            )
                        )
                    )
                )
            )
            ->add(
                'amount',
                'money',
                array(
                    'constraints' => array(
                    new NotBlank()
                ))
            )
            ->add(
                'isEnabled',
                'text',
                array()
            )
            ->add(
                'expirationDate',
                'text',
                array(
                    'constraints' => array(
                        new NotBlank(),
                        new Callback(array(
                            "methods" => array(
                                array($this, "checkLocalizedDate"),
                            ),
                        ))
                    )
                )
            )
            ->add(
                'isCumulative',
                'text',
                array()
            )
            ->add(
                'isRemovingPostage',
                'text',
                array()
            )
            ->add(
                'isAvailableOnSpecialOffers',
                'text',
                array()
            )
            ->add(
                'maxUsage',
                'text',
                array(
                    'constraints' => array(
                        new NotBlank(),
                        new GreaterThanOrEqual(['value' => -1])
                    )
                )
            )
            ->add(
                'locale',
                'hidden',
                array(
                    'constraints' => array(
                        new NotBlank()
                    )
                )
            );
    }

    /**
     * Validate a date entered with the default Language date format.
     *
     * @param string                    $value
     * @param ExecutionContextInterface $context
     */
    public function checkLocalizedDate($value, ExecutionContextInterface $context)
    {
        $format = LangQuery::create()->findOneByByDefault(true)->getDateFormat();

        if (false === \DateTime::createFromFormat($format, $value)) {
            $context->addViolation(Translator::getInstance()->trans("Date '%date' is invalid, please enter a valid date using %fmt format", [
                '%fmt' => $format,
                '%date' => $value
            ]));
        }
    }

    /**
     * Get form name
     *
     * @return string
     */
    public function getName()
    {
        return 'thelia_coupon_creation';
    }
}


@@ -68,7 +68,9 @@ class DbalProductValueChangedEventProjector
             $this->insert($productId, $attributeId, $phrase);
         } elseif ($value instanceof StringCollectionValue) {
             foreach ($value->getValue() as $language => $phrase) {
-                $this->insert($productId, $attributeId, $phrase, $language);
+                if ($phrase) {
+                    $this->insert($productId, $attributeId, $phrase, $language);
+                }
             }
         } elseif ($value instanceof TranslatableStringValue) {
             $translation = $value->getValue();

<?php

/**
 * Copyright © Bold Brand Commerce Sp. z o.o. All rights reserved.
 * See LICENSE.txt for license details.
 */

declare(strict_types = 1);

namespace Ergonode\Product\Infrastructure\Persistence\Projector;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DBALException;
use Ergonode\Product\Domain\Event\ProductValueChangedEvent;
use Ergonode\SharedKernel\Domain\Aggregate\AttributeId;
use Ergonode\Value\Domain\ValueObject\StringCollectionValue;
use Ergonode\Value\Domain\ValueObject\StringValue;
use Ergonode\Value\Domain\ValueObject\TranslatableStringValue;
use Ergonode\Value\Domain\ValueObject\ValueInterface;
use Ramsey\Uuid\Uuid;

/**
 */
class DbalProductValueChangedEventProjector
{
    private const TABLE_PRODUCT_VALUE = 'product_value';
    private const TABLE_VALUE_TRANSLATION = 'value_translation';

    /**
     * @var Connection
     */
    private Connection $connection;

    /**
     * @param Connection $connection
     */
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    /**
     * @param ProductValueChangedEvent $event
     *
     * @throws DBALException
     */
    public function __invoke(ProductValueChangedEvent $event): void
    {
        $productId = $event->getAggregateId()->getValue();
        $attributeId = AttributeId::fromKey($event->getAttributeCode()->getValue())->getValue();

        $this->delete($productId, $attributeId);
        $this->insertValue($productId, $attributeId, $event->getTo());
    }

    /**
     * @param string         $productId
     * @param string         $attributeId
     * @param ValueInterface $value
     *
     * @throws DBALException
     */
    private function insertValue(string $productId, string $attributeId, ValueInterface $value): void
    {
        if ($value instanceof StringValue) {
            $array = $value->getValue();
            $phrase = reset($array);
            $this->insert($productId, $attributeId, $phrase);
        } elseif ($value instanceof StringCollectionValue) {
            foreach ($value->getValue() as $language => $phrase) {
                $this->insert($productId, $attributeId, $phrase, $language);
            }
        } elseif ($value instanceof TranslatableStringValue) {
            $translation = $value->getValue();
            foreach ($translation as $language => $phrase) {
                $this->insert($productId, $attributeId, $phrase, $language);
            }
        } else {
            throw new \RuntimeException(
                sprintf(sprintf('Unknown Value class "%s"', \get_class($value->getValue())))
            );
        }
    }

    /**
     * @param string      $productId
     * @param string      $attributeId
     * @param string|null $value
     * @param string|null $language
     *
     * @throws DBALException
     */
    private function insert(string $productId, string $attributeId, ?string $value, ?string $language = null): void
    {
        $valueId = Uuid::uuid5(ValueInterface::NAMESPACE, implode('|', [$value, $language]));

        $qb = $this->connection->createQueryBuilder();
        $result = $qb->select('*')
            ->from(self::TABLE_VALUE_TRANSLATION)
            ->where($qb->expr()->eq('id', ':id'))
            ->setParameter(':id', $valueId->toString())
            ->execute()
            ->fetch();

        if (false === $result) {
            $this->connection->executeQuery(
                'INSERT INTO value_translation (id, value_id, value, language) '.
                ' VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING',
                [$valueId->toString(), $valueId->toString(), $value, $language ?: null]
            );
        }

        $this->connection->insert(
            self::TABLE_PRODUCT_VALUE,
            [
                'product_id' => $productId,
                'attribute_id' => $attributeId,
                'value_id' => $valueId,
            ]
        );
    }

    /**
     * @param string $productId
     * @param string $attributeId
     *
     * @throws DBALException
     */
    private function delete(string $productId, string $attributeId): void
    {
        $this->connection->delete(
            self::TABLE_PRODUCT_VALUE,
            [
                'product_id' => $productId,
                'attribute_id' => $attributeId,
            ]
        );
    }
}

This is not a solution, this only ensures that an empty value is not saved in this projector. Empty value will remain in the domain object and will appear in other projectors based on this event.