@@ -37,7 +37,12 @@ class WriteConcern {
   /**
    * Construct a WriteConcern given an options object.
    *
-   * @param {object} options The options object from which to extract the write concern.
+   * @param {object} [options] The options object from which to extract the write concern.
+   * @param {(number|string)} [options.w] **Deprecated** Use `options.writeConcern` instead
+   * @param {number} [options.wtimeout] **Deprecated** Use `options.writeConcern` instead
+   * @param {boolean} [options.j] **Deprecated** Use `options.writeConcern` instead
+   * @param {boolean} [options.fsync] **Deprecated** Use `options.writeConcern` instead
+   * @param {object|WriteConcern} [options.writeConcern] Specify write concern settings.
    * @return {WriteConcern}
    */
   static fromOptions(options) {

'use strict';

const kWriteConcernKeys = new Set(['w', 'wtimeout', 'j', 'fsync']);

/**
 * The **WriteConcern** class is a class that represents a MongoDB WriteConcern.
 * @class
 * @property {(number|string)} w The write concern
 * @property {number} wtimeout The write concern timeout
 * @property {boolean} j The journal write concern
 * @property {boolean} fsync The file sync write concern
 * @see https://docs.mongodb.com/manual/reference/write-concern/index.html
 */
class WriteConcern {
  /**
   * Constructs a WriteConcern from the write concern properties.
   * @param {(number|string)} [w] The write concern
   * @param {number} [wtimeout] The write concern timeout
   * @param {boolean} [j] The journal write concern
   * @param {boolean} [fsync] The file sync write concern
   */
  constructor(w, wtimeout, j, fsync) {
    if (w != null) {
      this.w = w;
    }
    if (wtimeout != null) {
      this.wtimeout = wtimeout;
    }
    if (j != null) {
      this.j = j;
    }
    if (fsync != null) {
      this.fsync = fsync;
    }
  }

  /**
   * Construct a WriteConcern given an options object.
   *
   * @param {object} options The options object from which to extract the write concern.
   * @return {WriteConcern}
   */
  static fromOptions(options) {
    if (
      options == null ||
      (options.writeConcern == null &&
        options.w == null &&
        options.wtimeout == null &&
        options.j == null &&
        options.fsync == null)
    ) {
      return;
    }

    if (options.writeConcern) {
      if (typeof options.writeConcern === 'string') {
        return new WriteConcern(options.writeConcern);
      }

      if (!Object.keys(options.writeConcern).some(key => kWriteConcernKeys.has(key))) {
        return;
      }

      return new WriteConcern(
        options.writeConcern.w,
        options.writeConcern.wtimeout,
        options.writeConcern.j,
        options.writeConcern.fsync
      );
    }

    return new WriteConcern(options.w, options.wtimeout, options.j, options.fsync);
  }
}

module.exports = WriteConcern;

Let's be consistent with the Deprecated tag in the docs, I think the use of bold is good so can we change the `DEPRECATED:` tags to match?


@@ -13,7 +13,7 @@ describe('has-visible-text', function() {
   });

   it('should return false if there is no visible text', function() {
-    var params = checkSetup('<object id="target"></object>');
+    var params = checkSetup('

');
     assert.isFalse(
       axe.testUtils
         .getCheckEvaluate('has-visible-text')

describe('has-visible-text', function() {
  'use strict';

  var fixture = document.getElementById('fixture');
  var checkSetup = axe.testUtils.checkSetup;

  var checkContext = axe.testUtils.MockCheckContext();

  afterEach(function() {
    fixture.innerHTML = '';
    axe._tree = undefined;
    checkContext.reset();
  });

  it('should return false if there is no visible text', function() {
    var params = checkSetup('<object id="target"></object>');
    assert.isFalse(
      axe.testUtils
        .getCheckEvaluate('has-visible-text')
        .apply(checkContext, params)
    );
  });

  it('should return false if there is text, but its hidden', function() {
    var params = checkSetup(
      '<object id="target">hello!</object>'
    );
    assert.isFalse(
      axe.testUtils
        .getCheckEvaluate('has-visible-text')
        .apply(checkContext, params)
    );
  });

  it('should return true if there is visible text', function() {
    var params = checkSetup('<object id="target">hello!</object>');
    assert.isTrue(
      axe.testUtils
        .getCheckEvaluate('has-visible-text')
        .apply(checkContext, params)
    );
  });

  describe('SerialVirtualNode', function() {
    it('should return false if element is not named from contents', function() {
      var node = new axe.SerialVirtualNode({
        nodeName: 'article'
      });

      assert.isFalse(
        axe.testUtils.getCheckEvaluate('has-visible-text')(null, {}, node)
      );
    });

    it('should return incomplete if no other properties are set', function() {
      var node = new axe.SerialVirtualNode({
        nodeName: 'button'
      });

      assert.isUndefined(
        axe.testUtils.getCheckEvaluate('has-visible-text')(null, {}, node)
      );
    });

    it('should return false if there is no visible text', function() {
      var node = new axe.SerialVirtualNode({
        nodeName: 'button'
      });
      node.children = [];

      assert.isFalse(
        axe.testUtils.getCheckEvaluate('has-visible-text')(null, {}, node)
      );
    });

    it('should return true if there is visible text', function() {
      var node = new axe.SerialVirtualNode({
        nodeName: 'object'
      });
      var child = new axe.SerialVirtualNode({
        nodeName: '#text',
        nodeType: 3,
        nodeValue: 'hello!'
      });
      node.children = [child];

      assert.isTrue(
        axe.testUtils.getCheckEvaluate('has-visible-text')(null, {}, node)
      );
    });
  });
});

Don't know why we did that. Clearly that's wrong.


@@ -15,7 +15,9 @@ const browsers = {
   chrome: tester((ua, vendor) => /Chrome/.test(ua) && /Google/.test(vendor)),
   edge: tester(ua => /Edge/.test(ua)),
   ie: tester(ua => /Trident/.test(ua)),
+  // eslint-disable-next-line
   ie8: tester(() => !(document.createTextNode('test').textContent)),
+  // eslint-disable-next-line
   ie9: tester(() => !!(document.documentMode)),
   mobile: tester(ua => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua)),
   safari: tester((ua, vendor) => /Safari/.test(ua) && /Apple Computer/.test(vendor)),

import { objectEach } from './object';

const tester = (testerFunc) => {
  const result = {
    value: false,
  };
  result.test = (ua, vendor) => {
    result.value = testerFunc(ua, vendor);
  };

  return result;
};

const browsers = {
  chrome: tester((ua, vendor) => /Chrome/.test(ua) && /Google/.test(vendor)),
  edge: tester(ua => /Edge/.test(ua)),
  ie: tester(ua => /Trident/.test(ua)),
  ie8: tester(() => !(document.createTextNode('test').textContent)),
  ie9: tester(() => !!(document.documentMode)),
  mobile: tester(ua => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua)),
  safari: tester((ua, vendor) => /Safari/.test(ua) && /Apple Computer/.test(vendor)),
};

export function setBrowserMeta({ userAgent = navigator.userAgent, vendor = navigator.vendor } = {}) {
  objectEach(browsers, ({ test }) => void test(userAgent, vendor));
}

setBrowserMeta();

export function isChrome() {
  return browsers.chrome.value;
}

export function isEdge() {
  return browsers.edge.value;
}

export function isIE() {
  return browsers.ie.value;
}

export function isIE8() {
  return browsers.ie8.value;
}

export function isIE9() {
  return browsers.ie9.value;
}

export function isMSBrowser() {
  return browsers.ie.value || browsers.edge.value;
}

export function isMobileBrowser() {
  return browsers.mobile.value;
}

export function isSafari() {
  return browsers.safari.value;
}

Maybe change from `eslint-disable-next-line` to `eslint-disable-next-line no-restricted-globals` here and in next occurs?


@@ -95,7 +95,7 @@ define(["loading", "dialogHelper", "dom", "jQuery", "components/libraryoptionsed

     function getFolderHtml(pathInfo, index) {
         var html = "";
-        return html += '
', html += '
', html += '
' + pathInfo.Path + "
", pathInfo.NetworkPath && (html += '
' + pathInfo.NetworkPath + "
"), html += "
", html += '<button type="button" is="paper-icon-button-light"" class="listItemButton btnRemovePath" data-index="' + index + '">remove_circle</button>', html += "
"
+        return html += '
', html += '
', html += '
' + pathInfo.Path + "
", pathInfo.NetworkPath && (html += '
' + pathInfo.NetworkPath + "
"), html += "
", html += '<button type="button" is="paper-icon-button-light"" class="listItemButton btnRemovePath" data-index="' + index + '">remove_circle</button>', html += "
"
     }

     function renderPaths(page) {

define(["loading", "dialogHelper", "dom", "jQuery", "components/libraryoptionseditor/libraryoptionseditor", "emby-toggle", "emby-input", "emby-select", "paper-icon-button-light", "listViewStyle", "formDialogStyle", "emby-button", "flexStyles"], function(loading, dialogHelper, dom, $, libraryoptionseditor) {
    "use strict";

    function onAddLibrary() {
        if (isCreating) return false;

        if (pathInfos.length == 0) {
            require(["alert"], function(alert) {
                alert({
                    text: Globalize.translate("PleaseAddAtLeastOneFolder"),
                    type: "error"
                })
            });
            return false;
        }

        isCreating = true;
        loading.show();

        var dlg = dom.parentWithClass(this, "dlg-librarycreator");
        var name = $("#txtValue", dlg).val();
        var type = $("#selectCollectionType", dlg).val();
        if (type == "mixed") type = null;
        var libraryOptions = libraryoptionseditor.getLibraryOptions(dlg.querySelector(".libraryOptions"));
        libraryOptions.PathInfos = pathInfos;
        ApiClient.addVirtualFolder(name, type, currentOptions.refresh, libraryOptions).then(function() {
            hasChanges = true;
            isCreating = false;
            loading.hide();
            dialogHelper.close(dlg);
        }, function() {
            require(["toast"], function(toast) {
                toast(Globalize.translate("ErrorAddingMediaPathToVirtualFolder"));
            });
            isCreating = false;
            loading.hide();
        });
        return false;
    }

    function getCollectionTypeOptionsHtml(collectionTypeOptions) {
        return collectionTypeOptions.map(function(i) {
            return '<option value="' + i.value + '">' + i.name + "</option>";
        }).join("");
    }

    function initEditor(page, collectionTypeOptions) {
        $("#selectCollectionType", page).html(getCollectionTypeOptionsHtml(collectionTypeOptions)).val("").on("change", function() {
            var value = this.value;
            var dlg = $(this).parents(".dialog")[0];
            libraryoptionseditor.setContentType(dlg.querySelector(".libraryOptions"), value == "mixed" ? "" : value);
            if (value) {
                dlg.querySelector(".libraryOptions").classList.remove("hide");
            } else {
                dlg.querySelector(".libraryOptions").classList.add("hide");
            }

            if (value != "mixed") {
                var index = this.selectedIndex;
                if (index != -1) {
                    var name = this.options[index].innerHTML.replace("*", "").replace("&", "&");
                    $("#txtValue", dlg).val(name);
                    var folderOption = collectionTypeOptions.filter(function(i) {
                        return i.value == value
                    })[0];
                    $(".collectionTypeFieldDescription", dlg).html(folderOption.message || "")
                }
            }
        });

        page.querySelector(".btnAddFolder").addEventListener("click", onAddButtonClick);
        page.querySelector(".btnSubmit").addEventListener("click", onAddLibrary);
        page.querySelector(".folderList").addEventListener("click", onRemoveClick);
        page.querySelector(".chkAdvanced").addEventListener("change", onToggleAdvancedChange);
    }

    function onToggleAdvancedChange() {
        var dlg = dom.parentWithClass(this, "dlg-librarycreator");
        libraryoptionseditor.setAdvancedVisible(dlg.querySelector(".libraryOptions"), this.checked);
    }

    function onAddButtonClick() {
        var page = dom.parentWithClass(this, "dlg-librarycreator");
        require(["directorybrowser"], function(directoryBrowser) {
            var picker = new directoryBrowser;
            picker.show({
                enableNetworkSharePath: true,
                callback: function(path, networkSharePath) {
                    path && addMediaLocation(page, path, networkSharePath);
                    picker.close();
                }
            })
        })
    }

    function getFolderHtml(pathInfo, index) {
        var html = "";
        return html += '
', html += '
', html += '
' + pathInfo.Path + "
", pathInfo.NetworkPath && (html += '
' + pathInfo.NetworkPath + "
"), html += "
", html += '<button type="button" is="paper-icon-button-light"" class="listItemButton btnRemovePath" data-index="' + index + '">remove_circle</button>', html += "
"
    }

    function renderPaths(page) {
        var foldersHtml = pathInfos.map(getFolderHtml).join("");
        var folderList = page.querySelector(".folderList");
        folderList.innerHTML = foldersHtml;
        foldersHtml ? folderList.classList.remove("hide") : folderList.classList.add("hide");
    }

    function addMediaLocation(page, path, networkSharePath) {
        var pathLower = path.toLowerCase();
        var pathFilter = pathInfos.filter(function(p) {
            return p.Path.toLowerCase() == pathLower;
        });
        if (!pathFilter.length) {
            var pathInfo = {
                Path: path
            };
            networkSharePath && (pathInfo.NetworkPath = networkSharePath);
            pathInfos.push(pathInfo);
            renderPaths(page);
        }
    }

    function onRemoveClick(e) {
        var button = dom.parentWithClass(e.target, "btnRemovePath");
        var index = parseInt(button.getAttribute("data-index"));
        var location = pathInfos[index].Path;
        var locationLower = location.toLowerCase();
        pathInfos = pathInfos.filter(function(p) {
            return p.Path.toLowerCase() != locationLower;
        });
        renderPaths(dom.parentWithClass(button, "dlg-librarycreator"));
    }

    function onDialogClosed() {
        currentResolve(hasChanges);
    }

    function initLibraryOptions(dlg) {
        libraryoptionseditor.embed(dlg.querySelector(".libraryOptions")).then(function() {
            $("#selectCollectionType", dlg).trigger("change");
            onToggleAdvancedChange.call(dlg.querySelector(".chkAdvanced"));
        })
    }

    function editor() {
        this.show = function(options) {
            return new Promise(function(resolve, reject) {
                currentOptions = options;
                currentResolve = resolve;
                hasChanges = false;
                var xhr = new XMLHttpRequest;
                xhr.open("GET", "components/medialibrarycreator/medialibrarycreator.template.html", true);
                xhr.onload = function(e) {
                    var template = this.response;
                    var dlg = dialogHelper.createDialog({
                        size: "medium-tall",
                        modal: false,
                        removeOnClose: true,
                        scrollY: false
                    });
                    dlg.classList.add("ui-body-a");
                    dlg.classList.add("background-theme-a");
                    dlg.classList.add("dlg-librarycreator");
                    dlg.classList.add("formDialog");
                    dlg.innerHTML = Globalize.translateDocument(template);
                    initEditor(dlg, options.collectionTypeOptions);
                    dlg.addEventListener("close", onDialogClosed);
                    dialogHelper.open(dlg);
                    dlg.querySelector(".btnCancel").addEventListener("click", function() {
                        dialogHelper.close(dlg)
                    });
                    pathInfos = [];
                    renderPaths(dlg);
                    initLibraryOptions(dlg);
                };
                xhr.send();
            });
        }
    }

    var pathInfos = [];
    var currentResolve;
    var currentOptions;

    var hasChanges = false;
    var isCreating = false;

    return editor
});


@@ -7,11 +7,18 @@ const sinon = require('sinon');
 class MockTopology extends EventEmitter {
   constructor() {
     super();
+    this.s = {
+      promiseLibrary: Promise
+    };
   }

   capabilities() {
     return {};
   }
+
+  hasSessionSupport() {
+    return false;
+  }
 }

 const test = {};

'use strict';

const EventEmitter = require('events');
const expect = require('chai').expect;
const sinon = require('sinon');

class MockTopology extends EventEmitter {
  constructor() {
    super();
  }

  capabilities() {
    return {};
  }
}

const test = {};
describe('Database', function() {
  before(() => {
    // NOTE: These modules are being used prior to test run. In order to monkey-patch them
    //       we must remove their cached versions.
    const resolvedUtils = require.resolve('../../lib/utils');
    const resolvedDb = require.resolve('../../lib/db');
    delete require.cache[resolvedUtils];
    delete require.cache[resolvedDb];
    test.utils = require('../../lib/utils');

    // create a sandbox for stub cleanup
    test.sandbox = sinon.sandbox.create();
  });

  afterEach(() => test.sandbox.restore());

  it('should ignore a readPreference for dropDatabase', {
    metadata: { requires: { topology: 'single' } },
    test: function() {
      sinon.stub(test.utils, 'executeOperation').callsFake((topology, operation, args) => {
        const options = args[args.length - 2];
        expect(options.readPreference).to.equal('primary');
      });

      const Db = require('../../lib/db');
      const db = new Db('fakeDb', new MockTopology(), { readPreference: 'nearest' });
      db.dropDatabase();
    }
  });
});

I'm not sure if this is the best way to ensure the `MockTopology` has a `promiseLibrary`, but I figured this was in the right direction.


@@ -61,7 +61,20 @@ export default Controller.extend({
             this.integration.rollbackAttributes();

             return transition.retry();
+        },
+
+        deleteIntegration() {
+            this.integration.destroyRecord();
+        },
+
+        confirmIntegrationDeletion() {
+            this.set('showDeleteIntegrationModal', true);
+        },
+
+        cancelIntegrationDeletion() {
+            this.set('showDeleteIntegrationModal', false);
         }
+
     },

     save: task(function* () {

import Controller from '@ember/controller';
import {alias} from '@ember/object/computed';
import {computed} from '@ember/object';
import {task, timeout} from 'ember-concurrency';

export default Controller.extend({
    integration: alias('model'),

    allWebhooks: computed(function () {
        return this.store.peekAll('webhook');
    }),

    filteredWebhooks: computed('allWebhooks.@each.{isNew,isDeleted}', function () {
        return this.allWebhooks.filter((webhook) => {
            let matchesIntegration = webhook.belongsTo('integration').id() === this.integration.id;

            return matchesIntegration
                && !webhook.isNew
                && !webhook.isDeleted;
        });
    }),

    actions: {
        save() {
            return this.save.perform();
        },

        toggleUnsavedChangesModal(transition) {
            let leaveTransition = this.leaveScreenTransition;

            if (!transition && this.showUnsavedChangesModal) {
                this.set('leaveScreenTransition', null);
                this.set('showUnsavedChangesModal', false);
                return;
            }

            if (!leaveTransition || transition.targetName === leaveTransition.targetName) {
                this.set('leaveScreenTransition', transition);

                // if a save is running, wait for it to finish then transition
                if (this.save.isRunning) {
                    return this.save.last.then(() => {
                        transition.retry();
                    });
                }

                // we genuinely have unsaved data, show the modal
                this.set('showUnsavedChangesModal', true);
            }
        },

        leaveScreen() {
            let transition = this.leaveScreenTransition;

            if (!transition) {
                this.notifications.showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'});
                return;
            }

            // roll back changes on model props
            this.integration.rollbackAttributes();

            return transition.retry();
        }
    },

    save: task(function* () {
        return yield this.integration.save();
    }),

    copyContentKey: task(function* () {
        this._copyInputTextToClipboard('input#content_key');
        yield timeout(3000);
    }),

    copyAdminKey: task(function* () {
        this._copyInputTextToClipboard('input#admin_key');
        yield timeout(3000);
    }),

    _copyInputTextToClipboard(selector) {
        let input = document.querySelector(selector);
        input.disabled = false;
        input.focus();
        input.select();
        document.execCommand('copy');
        input.disabled = true;
    }
});

Similar pattern to webhooks, feels unnecessary as there's only ever one integration on a page. But it's useful to show the modal, maybe have a `showIntegrationDeleteModal` instead and set to true/false...?


@@ -56,7 +56,7 @@ class TopProductFacade
     }

     /**
-     * @param $domainId
+     * @param int $domainId
      * @param \Shopsys\FrameworkBundle\Model\Product\Product[] $products
      */
     public function saveTopProductsForDomain($domainId, array $products)

<?php

namespace Shopsys\FrameworkBundle\Model\Product\TopProduct;

use Doctrine\ORM\EntityManagerInterface;

class TopProductFacade
{
    /**
     * @var \Doctrine\ORM\EntityManagerInterface
     */
    protected $em;

    /**
     * @var \Shopsys\FrameworkBundle\Model\Product\TopProduct\TopProductRepository
     */
    protected $topProductRepository;

    /**
     * @var \Shopsys\FrameworkBundle\Model\Product\TopProduct\TopProductFactoryInterface
     */
    protected $topProductFactory;

    /**
     * @param \Doctrine\ORM\EntityManagerInterface $em
     * @param \Shopsys\FrameworkBundle\Model\Product\TopProduct\TopProductRepository $topProductRepository
     * @param \Shopsys\FrameworkBundle\Model\Product\TopProduct\TopProductFactoryInterface $topProductFactory
     */
    public function __construct(
        EntityManagerInterface $em,
        TopProductRepository $topProductRepository,
        TopProductFactoryInterface $topProductFactory
    ) {
        $this->em = $em;
        $this->topProductRepository = $topProductRepository;
        $this->topProductFactory = $topProductFactory;
    }

    /**
     * @param int $domainId
     * @return \Shopsys\FrameworkBundle\Model\Product\TopProduct\TopProduct[]
     */
    public function getAll($domainId)
    {
        return $this->topProductRepository->getAll($domainId);
    }

    /**
     * @param int $domainId
     * @param \Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup $pricingGroup
     * @return \Shopsys\FrameworkBundle\Model\Product\Product[]
     */
    public function getAllOfferedProducts($domainId, $pricingGroup)
    {
        return $this->topProductRepository->getOfferedProductsForTopProductsOnDomain($domainId, $pricingGroup);
    }

    /**
     * @param $domainId
     * @param \Shopsys\FrameworkBundle\Model\Product\Product[] $products
     */
    public function saveTopProductsForDomain($domainId, array $products)
    {
        $oldTopProducts = $this->topProductRepository->getAll($domainId);
        foreach ($oldTopProducts as $oldTopProduct) {
            $this->em->remove($oldTopProduct);
        }
        $this->em->flush($oldTopProducts);

        $topProducts = [];
        $position = 1;
        foreach ($products as $product) {
            $topProduct = $this->topProductFactory->create($product, $domainId, $position++);
            $this->em->persist($topProduct);
            $topProducts[] = $topProduct;
        }
        $this->em->flush($topProducts);
    }
}

hmm, strange a few line below $this->em->flush with parameter seems like no problem. Why is that so ? Because it is array?


@@ -77,10 +77,12 @@ class MediaAdminController extends Controller
         $datagrid->setValue('context', null, $context);

         // retrieve the main category for the tree view
-        $category = $this->container->get('sonata.classification.manager.category')->getRootCategory($context);
+        $rootCategory = $this->container->get('sonata.classification.manager.category')->getRootCategory($context);
+        // This should be safe as the root category has to exist for a given context but I do not like fatal errors
+        $rootCategoryId = !empty($rootCategory) ? $rootCategory->getId() : null;

         if (!$filters) {
-            $datagrid->setValue('category', null, $category);
+            $datagrid->setValue('category', null, $rootCategoryId);
         }
         if ($request->get('category')) {
             $categoryByContext = $this->container->get('sonata.classification.manager.category')->findOneBy(array(

<?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\Controller;

use Sonata\AdminBundle\Controller\CRUDController as Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

class MediaAdminController extends Controller
{
    /**
     * {@inheritdoc}
     */
    public function createAction(Request $request = null)
    {
        if (false === $this->admin->isGranted('CREATE')) {
            throw new AccessDeniedException();
        }

        if (!$request->get('provider') && $request->isMethod('get')) {
            return $this->render('SonataMediaBundle:MediaAdmin:select_provider.html.twig', array(
                'providers' => $this->get('sonata.media.pool')->getProvidersByContext($request->get('context', $this->get('sonata.media.pool')->getDefaultContext())),
                'base_template' => $this->getBaseTemplate(),
                'admin' => $this->admin,
                'action' => 'create',
            ));
        }

        return parent::createAction();
    }

    /**
     * {@inheritdoc}
     */
    public function render($view, array $parameters = array(), Response $response = null, Request $request = null)
    {
        $parameters['media_pool'] = $this->container->get('sonata.media.pool');
        $parameters['persistent_parameters'] = $this->admin->getPersistentParameters();

        return parent::render($view, $parameters, $response, $request);
    }

    /**
     * {@inheritdoc}
     */
    public function listAction(Request $request = null)
    {
        if (false === $this->admin->isGranted('LIST')) {
            throw new AccessDeniedException();
        }

        if ($listMode = $request->get('_list_mode', 'mosaic')) {
            $this->admin->setListMode($listMode);
        }

        $datagrid = $this->admin->getDatagrid();

        $filters = $request->get('filter');

        // set the default context
        if (!$filters || !array_key_exists('context', $filters)) {
            $context = $this->admin->getPersistentParameter('context', $this->get('sonata.media.pool')->getDefaultContext());
        } else {
            $context = $filters['context']['value'];
        }

        $datagrid->setValue('context', null, $context);

        // retrieve the main category for the tree view
        $category = $this->container->get('sonata.classification.manager.category')->getRootCategory($context);

        if (!$filters) {
            $datagrid->setValue('category', null, $category);
        }
        if ($request->get('category')) {
            $categoryByContext = $this->container->get('sonata.classification.manager.category')->findOneBy(array(
                'id' => (int) $request->get('category'),
                'context' => $context,
            ));

            if (!empty($categoryByContext)) {
                $datagrid->setValue('category', null, $categoryByContext);
            } else {
                $datagrid->setValue('category', null, $category);
            }
        }

        $formView = $datagrid->getForm()->createView();

        // set the theme for the current Admin Form
        $this->get('twig')->getExtension('form')->renderer->setTheme($formView, $this->admin->getFilterTheme());

        return $this->render($this->admin->getTemplate('list'), array(
            'action' => 'list',
            'form' => $formView,
            'datagrid' => $datagrid,
            'root_category' => $category,
            'csrf_token' => $this->getCsrfToken('sonata.batch'),
        ));
    }
}

Why not throw an exception instead then?


@@ -76,7 +76,7 @@ class CustomerUserRepository
     {
         $customerUser = $this->findById($id);
         if ($customerUser === null) {
-            throw new \Shopsys\FrameworkBundle\Model\Customer\Exception\CustomerUserNotFoundException($id);
+            throw new \Shopsys\FrameworkBundle\Model\Customer\Exception\CustomerUserNotFoundException((string)$id);
         }
         return $customerUser;
     }

<?php

namespace Shopsys\FrameworkBundle\Model\Customer\User;

use Doctrine\ORM\EntityManagerInterface;
use Shopsys\FrameworkBundle\Component\String\DatabaseSearching;
use Shopsys\FrameworkBundle\Form\Admin\QuickSearch\QuickSearchFormData;
use Shopsys\FrameworkBundle\Model\Customer\BillingAddress;
use Shopsys\FrameworkBundle\Model\Customer\Exception\CustomerUserNotFoundException;
use Shopsys\FrameworkBundle\Model\Order\Order;
use Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup;

class CustomerUserRepository
{
    /**
     * @var \Doctrine\ORM\EntityManagerInterface
     */
    protected $em;

    /**
     * @param \Doctrine\ORM\EntityManagerInterface $entityManager
     */
    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->em = $entityManager;
    }

    /**
     * @return \Doctrine\ORM\EntityRepository
     */
    protected function getCustomerUserRepository()
    {
        return $this->em->getRepository(CustomerUser::class);
    }

    /**
     * @param string $email
     * @param int $domainId
     *
     * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser|null
     */
    public function findCustomerUserByEmailAndDomain($email, $domainId)
    {
        return $this->getCustomerUserRepository()->findOneBy([
            'email' => mb_strtolower($email),
            'domainId' => $domainId,
        ]);
    }

    /**
     * @param string $email
     * @param int $domainId
     *
     * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser|null
     */
    public function getCustomerUserByEmailAndDomain($email, $domainId)
    {
        $customerUser = $this->findCustomerUserByEmailAndDomain($email, $domainId);

        if ($customerUser === null) {
            throw new \Shopsys\FrameworkBundle\Model\Customer\Exception\CustomerUserNotFoundByEmailAndDomainException(
                $email,
                $domainId
            );
        }

        return $customerUser;
    }

    /**
     * @param int $id
     *
     * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser
     */
    public function getCustomerUserById($id)
    {
        $customerUser = $this->findById($id);
        if ($customerUser === null) {
            throw new \Shopsys\FrameworkBundle\Model\Customer\Exception\CustomerUserNotFoundException($id);
        }
        return $customerUser;
    }

    /**
     * @param int $id
     *
     * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser|null
     */
    public function findById($id)
    {
        return $this->getCustomerUserRepository()->find($id);
    }

    /**
     * @param int $id
     * @param string $loginToken
     *
     * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser|null
     */
    public function findByIdAndLoginToken($id, $loginToken)
    {
        return $this->getCustomerUserRepository()->findOneBy([
            'id' => $id,
            'loginToken' => $loginToken,
        ]);
    }

    /**
     * @param int $domainId
     * @param \Shopsys\FrameworkBundle\Form\Admin\QuickSearch\QuickSearchFormData $quickSearchData
     * @return \Doctrine\ORM\QueryBuilder
     */
    public function getCustomerUserListQueryBuilderByQuickSearchData(
        $domainId,
        QuickSearchFormData $quickSearchData
    ) {
        $queryBuilder = $this->em->createQueryBuilder()
            ->select('
                u.id,
                u.email,
                u.telephone,
                MAX(pg.name) AS pricingGroup,
                MAX(ba.city) city,
                MAX(CASE WHEN ba.companyCustomer = true
                        THEN ba.companyName
                        ELSE CONCAT(u.lastName, \' \', u.firstName)
                    END) AS name,
                COUNT(o.id) ordersCount,
                SUM(o.totalPriceWithVat) ordersSumPrice,
                MAX(o.createdAt) lastOrderAt')
            ->from(CustomerUser::class, 'u')
            ->where('u.domainId = :selectedDomainId')
            ->setParameter('selectedDomainId', $domainId)
            ->join('u.customer', 'c')
            ->leftJoin(BillingAddress::class, 'ba', 'WITH', 'c.id = ba.customer')
            ->leftJoin(Order::class, 'o', 'WITH', 'o.customerUser = u.id AND o.deleted = :deleted')
            ->setParameter('deleted', false)
            ->leftJoin(PricingGroup::class, 'pg', 'WITH', 'pg.id = u.pricingGroup')
            ->groupBy('u.id');

        if ($quickSearchData->text !== null && $quickSearchData->text !== '') {
            $queryBuilder
                ->andWhere('
                    (
                        NORMALIZE(u.lastName) LIKE NORMALIZE(:text)
                        OR
                        NORMALIZE(u.email) LIKE NORMALIZE(:text)
                        OR
                        NORMALIZE(ba.companyName) LIKE NORMALIZE(:text)
                        OR
                        NORMALIZE(u.telephone) LIKE :text
                    )');
            $querySearchText = DatabaseSearching::getFullTextLikeSearchString($quickSearchData->text);
            $queryBuilder->setParameter('text', $querySearchText);
        }

        return $queryBuilder;
    }

    /**
     * @param \Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup $oldPricingGroup
     * @param \Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup $newPricingGroup
     */
    public function replaceCustomerUsersPricingGroup(PricingGroup $oldPricingGroup, PricingGroup $newPricingGroup)
    {
        $this->em->createQueryBuilder()
            ->update(CustomerUser::class, 'u')
            ->set('u.pricingGroup', ':newPricingGroup')->setParameter('newPricingGroup', $newPricingGroup)
            ->where('u.pricingGroup = :oldPricingGroup')->setParameter('oldPricingGroup', $oldPricingGroup)
            ->getQuery()->execute();
    }

    /**
     * @param string $uuid
     * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser
     */
    public function getOneByUuid(string $uuid): CustomerUser
    {
        /** @var \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser|null $customerUser */
        $customerUser = $this->getCustomerUserRepository()->findOneBy(['uuid' => $uuid]);

        if ($customerUser === null) {
            throw new CustomerUserNotFoundException('Customer with UUID ' . $uuid . ' does not exist.');
        }

        return $customerUser;
    }
}

Well you should probably send nothing here right?


@@ -38,6 +38,28 @@ namespace VuFind\Hierarchy\TreeDataSource;
  */
 class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager
 {
+    /**
+     * Default plugin aliases.
+     *
+     * @var array
+     */
+    protected $aliases = [
+        'solr' => 'VuFind\Hierarchy\TreeDataSource\Solr',
+        'xmlfile' => 'VuFind\Hierarchy\TreeDataSource\XMLFile',
+    ];
+
+    /**
+     * Default plugin factories.
+     *
+     * @var array
+     */
+    protected $factories = [
+        'VuFind\Hierarchy\TreeDataSource\Solr' =>
+            'VuFind\Hierarchy\TreeDataSource\Factory::getSolr',
+        'VuFind\Hierarchy\TreeDataSource\XMLFile' =>
+            'Zend\ServiceManager\Factory\InvokableFactory',
+    ];
+
     /**
      * Return the name of the base class or interface that plug-ins must conform
      * to.

<?php
/**
 * Hierarchy tree data source plugin manager
 *
 * PHP version 5
 *
 * Copyright (C) Villanova University 2010.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @category VuFind
 * @package  HierarchyTree_DataSource
 * @author   Demian Katz <demian.katz@villanova.edu>
 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
 * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
 */
namespace VuFind\Hierarchy\TreeDataSource;

/**
 * Hierarchy tree data source plugin manager
 *
 * @category VuFind
 * @package  HierarchyTree_DataSource
 * @author   Demian Katz <demian.katz@villanova.edu>
 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
 * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
 */
class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager
{
    /**
     * Return the name of the base class or interface that plug-ins must conform
     * to.
     *
     * @return string
     */
    protected function getExpectedInterface()
    {
        return 'VuFind\Hierarchy\TreeDataSource\AbstractBase';
    }
}

Might be worth replacing this static factory with a non-static one (i.e. SolrFactory, implementing FactoryInterface).


@@ -94,8 +94,8 @@ abstract class AbstractSolrTask extends AbstractTask {
     public function __sleep()
     {
         $properties = get_object_vars($this);
-        // avoid serialization if the site object
-        unset($properties['site']);
+        // avoid serialization if the site and logger object
+        unset($properties['site'], $properties['logger']);
         return array_keys($properties);
     }
-}
+}

<?php
namespace ApacheSolrForTypo3\Solr\Task;

/***************************************************************
 *  Copyright notice
 *
 *  (c) 2017 Timo Hund <timo.hund@dkd.de>
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
 *  free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *
 *  This script 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 General Public License for more details.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/

use ApacheSolrForTypo3\Solr\Domain\Site\SiteRepository;
use ApacheSolrForTypo3\Solr\Domain\Site\Site;
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\Task\AbstractTask;

/**
 * Abstract scheduler task for solr scheduler tasks, contains the logic to
 * retrieve the site, avoids serialization of site, when scheduler task is saved.
 *
 * @package ApacheSolrForTypo3\Solr\Task
 */
abstract class AbstractSolrTask extends AbstractTask {
    /**
     * The site this task is supposed to initialize the index queue for.
     *
     * @var Site
     */
    protected $site;

    /**
     * The rootPageId of the site that should be reIndexed
     *
     * @var integer
     */
    protected $rootPageId;

    /**
     * @return int
     */
    public function getRootPageId()
    {
        return $this->rootPageId;
    }

    /**
     * @param int $rootPageId
     */
    public function setRootPageId($rootPageId)
    {
        $this->rootPageId = $rootPageId;
    }

    /**
     * @return Site
     */
    public function getSite()
    {
        if (!is_null($this->site)) {
            return $this->site;
        }

        try {
            /** @var $siteRepository SiteRepository */
            $siteRepository = GeneralUtility::makeInstance(SiteRepository::class);
            $this->site = $siteRepository->getSiteByRootPageId($this->rootPageId);
        } catch (\InvalidArgumentException $e) {
            $logger = GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
            $logger->log(SolrLogManager::ERROR, 'Scheduler task tried to get invalid site');
        }

        return $this->site;
    }

    /**
     * @return array
     */
    public function __sleep()
    {
        $properties = get_object_vars($this);
        // avoid serialization if the site object
        unset($properties['site']);
        return array_keys($properties);
    }
}

Maybe you can correct the typo "if" too :)


@@ -4,6 +4,10 @@ declare(strict_types=1);

 use Doctrine\Common\Annotations\AnnotationRegistry;

+$symfonyDumpFunctionPath = 'vendor/symfony/var-dumper/Resources/functions/dump.php';
+
+file_exists(__DIR__ . '/../' . $symfonyDumpFunctionPath) ? require_once __DIR__ . '/../' . $symfonyDumpFunctionPath : require_once __DIR__ . '/../../' . $symfonyDumpFunctionPath;
+
 /* @var \Composer\Autoload\ClassLoader $loader */
 $loader = file_exists(__DIR__ . '/../vendor/autoload.php') ? require __DIR__ . '/../vendor/autoload.php' : require __DIR__ . '/../../vendor/autoload.php';

<?php

declare(strict_types=1);

use Doctrine\Common\Annotations\AnnotationRegistry;

/* @var \Composer\Autoload\ClassLoader $loader */
$loader = file_exists(__DIR__ . '/../vendor/autoload.php') ? require __DIR__ . '/../vendor/autoload.php' : require __DIR__ . '/../../vendor/autoload.php';

AnnotationRegistry::registerLoader([$loader, 'loadClass']);

return $loader;

this might kill whole application if `var-dumper` will not be installed. What about two separate file_exists conditions (monorepo/project)?


@@ -17,7 +17,7 @@ const SiteNotFound = "not found"
 const SiteDirMissing = "app directory missing"

 // SiteConfigMissing defines the string used to denote when a site is missing its .ddev/config.yml file.
-const SiteConfigMissing = ".ddev/config.yml missing"
+const SiteConfigMissing = ".ddev/config.yaml missing"

 // SiteStopped defines the string used to denote when a site is in the stopped state.
 const SiteStopped = "stopped"

package platform

import "fmt"
import (
	"strings"

	"github.com/fsouza/go-dockerclient"
)

// SiteRunning defines the string used to denote running sites.
const SiteRunning = "running"

// SiteNotFound defines the string used to denote a site where the containers were not found/do not exist.
const SiteNotFound = "not found"

// SiteDirMissing defines the string used to denote when a site is missing its application directory.
const SiteDirMissing = "app directory missing"

// SiteConfigMissing defines the string used to denote when a site is missing its .ddev/config.yml file.
const SiteConfigMissing = ".ddev/config.yml missing"

// SiteStopped defines the string used to denote when a site is in the stopped state.
const SiteStopped = "stopped"

// App is an interface apps for Drud Local must implement to use shared functionality
type App interface {
	Init(string) error
	Describe() (map[string]interface{}, error)
	GetType() string
	AppRoot() string
	GetName() string
	Start() error
	Stop() error
	DockerEnv()
	DockerComposeYAMLPath() string
	Down(removeData bool) error
	CreateSettingsFile() error
	HostName() string
	URL() string
	Import() error
	ImportDB(imPath string, extPath string) error
	ImportFiles(imPath string, extPath string) error
	SiteStatus() string
	FindContainerByType(containerType string) (docker.APIContainers, error)
	// Returns err, stdout, stderr
	Exec(service string, cmd ...string) (string, string, error)
	ExecWithTty(service string, cmd ...string) error
	Logs(service string, follow bool, timestamps bool, tail string) error
}

// PluginMap maps the name of the plugins to their implementation.
var PluginMap = map[string]App{
	"local": &LocalApp{},
}

// GetPluginApp will return an application of the type specified by pluginType
func GetPluginApp(pluginType string) (App, error) {
	switch strings.ToLower(pluginType) {
	case "local":
		return &LocalApp{}, nil
	default:
		return nil, fmt.Errorf("could not find plugin type %s", pluginType)
	}
}

Hmm, thought this would happen if .ddev was missing or docker_compose.yml was missing? Fine with me, don't think it's that important.


@@ -3,7 +3,7 @@ namespace Psalm\Tests\Provider;

 use PhpParser;

-class ParserInstanceCacheProvider extends \Psalm\Provider\ParserCacheProvider
+class ParserInstanceCacheProvider extends \Psalm\Internal\Provider\ParserCacheProvider
 {
     /**
      * @var array<string, string>

<?php
namespace Psalm\Tests\Provider;

use PhpParser;

class ParserInstanceCacheProvider extends \Psalm\Provider\ParserCacheProvider
{
    /**
     * @var array<string, string>
     */
    private $file_contents_cache = [];

    /**
     * @var array<string, string>
     */
    private $file_content_hash = [];

    /**
     * @var array<string, array<int, PhpParser\Node\Stmt>>
     */
    private $statements_cache = [];

    /**
     * @var array<string, float>
     */
    private $statements_cache_time = [];

    /**
     * @var int
     */
    private $last_good_run = 0;

    public function __construct()
    {
    }

    public function loadStatementsFromCache($file_path, $file_modified_time, $file_content_hash)
    {
        if (isset($this->statements_cache[$file_path])
            && $this->statements_cache_time[$file_path] >= $file_modified_time
            && $this->file_content_hash[$file_path] === $file_content_hash
        ) {
            return $this->statements_cache[$file_path];
        }

        return null;
    }

    /**
     * @param  string   $file_content_hash
     * @param  string   $file_path
     * @param mixed $file_modified_time
     *
     * @return array<int, PhpParser\Node\Stmt>|null
     */
    public function loadExistingStatementsFromCache($file_path)
    {
        if (isset($this->statements_cache[$file_path])) {
            return $this->statements_cache[$file_path];
        }

        return null;
    }

    /**
     * @param  string                           $file_path
     * @param  string                           $file_content_hash
     * @param  array<int, PhpParser\Node\Stmt>  $stmts
     * @param  bool                             $touch_only
     *
     * @return void
     */
    public function saveStatementsToCache($file_path, $file_content_hash, array $stmts, $touch_only)
    {
        $this->statements_cache[$file_path] = $stmts;
        $this->statements_cache_time[$file_path] = microtime(true);
        $this->file_content_hash[$file_path] = $file_content_hash;
    }

    /**
     * @param  string   $file_path
     *
     * @return string|null
     */
    public function loadExistingFileContentsFromCache($file_path)
    {
        if (isset($this->file_contents_cache[$file_path])) {
            return $this->file_contents_cache[$file_path];
        }

        return null;
    }

    /**
     * @param  string  $file_path
     * @param  string  $file_contents
     *
     * @return void
     */
    public function cacheFileContents($file_path, $file_contents)
    {
        $this->file_contents_cache[$file_path] = $file_contents;
    }

    /**
     * @return int
     */
    public function getLastGoodRun()
    {
        return $this->last_good_run;
    }

    /**
     * @param float $start_time
     *
     * @return void
     */
    public function processSuccessfulRun($start_time)
    {
        $this->last_good_run = (int) $start_time;
    }

    /**
     * @return bool
     */
    public function canDiffFiles()
    {
        return $this->last_good_run > 0;
    }
}

nit: Would it make more sense to move tests/Provider to tests/Internal/Provider (etc.) so that Psalm\Tests\Internal\Foo contains overrides of Psalm\Internal\Foo (e.g. if both Psalm\Provider and Psalm\Internal\Provider exist later)


@@ -4,13 +4,30 @@ declare(strict_types=1);

 namespace Bolt\DataFixtures;

+use Bolt\Configuration\Areas;
+use Bolt\Configuration\Config;
 use Doctrine\Bundle\FixturesBundle\Fixture;
+use Symfony\Component\Finder\Finder;
+use Tightenco\Collect\Support\Collection;
+use Webmozart\PathUtil\Path;

 abstract class BaseFixture extends Fixture
 {
     private $referencesIndex = [];
     private $taxonomyIndex = [];

+    /** @var Config */
+    protected $config;
+
+    /** @var Areas */
+    protected $areas;
+
+    public function __construct(Config $config, Areas $areas)
+    {
+        $this->config = $config;
+        $this->areas = $areas;
+    }
+
     protected function getRandomReference(string $entityName)
     {
         if (isset($this->referencesIndex[$entityName]) === false) {

<?php

declare(strict_types=1);

namespace Bolt\DataFixtures;

use Doctrine\Bundle\FixturesBundle\Fixture;

abstract class BaseFixture extends Fixture
{
    private $referencesIndex = [];
    private $taxonomyIndex = [];

    protected function getRandomReference(string $entityName)
    {
        if (isset($this->referencesIndex[$entityName]) === false) {
            $this->referencesIndex[$entityName] = [];

            foreach (array_keys($this->referenceRepository->getReferences()) as $key) {
                if (mb_strpos($key, $entityName.'_') === 0) {
                    $this->referencesIndex[$entityName][] = $key;
                }
            }
        }
        if (empty($this->referencesIndex[$entityName])) {
            throw new \Exception(sprintf('Cannot find any references for Entity "%s"', $entityName));
        }
        $randomReferenceKey = array_rand($this->referencesIndex[$entityName], 1);

        return $this->getReference($this->referencesIndex[$entityName][$randomReferenceKey]);
    }

    protected function getRandomTaxonomies(string $type, int $amount): array
    {
        if (empty($this->taxonomyIndex)) {
            foreach (array_keys($this->referenceRepository->getReferences()) as $key) {
                if (mb_strpos($key, 'taxonomy_') === 0) {
                    $tuples = explode('_', $key);
                    $this->taxonomyIndex[$tuples[1]][] = $key;
                }
            }
        }

        if (empty($this->taxonomyIndex[$type])) {
            return [];
        }

        $taxonomies = [];

        foreach ((array) array_rand($this->taxonomyIndex[$type], $amount) as $key) {
            $taxonomies[] = $this->getReference($this->taxonomyIndex[$type][$key]);
        }

        return $taxonomies;
    }
}

Why this constructor is added here when none of these dependencies is used in this class? Areas should not be a part of BaseFixture. move it to trait or to another abstract class


@@ -52,7 +52,8 @@ if (TYPO3_MODE == 'BE') {
         '',
         [
             // An array holding the controller-action-combinations that are accessible
-            'Administration' => 'index,setSite,setCore,noSiteAvailable'
+            'Administration' => 'index,setSite,setCore,noSiteAvailable',
+            'Backend\\Web\\Info\\ApacheSolrDocument' => 'index'
         ],
         [
             'access' => 'admin',

<?php
if (!defined('TYPO3_MODE')) {
    die('Access denied.');
}

# ----- # ----- # ----- # ----- # ----- # ----- # ----- # ----- # ----- #

// add search plugin to content element wizard
if (TYPO3_MODE == 'BE') {
    $TBE_MODULES_EXT['xMOD_db_new_content_el']['addElClasses']['ApacheSolrForTypo3\\Solr\\Backend\\ContentElementWizardIconProvider'] =
        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($_EXTKEY) . 'Classes/Backend/ContentElementWizardIconProvider.php';
}
# ----- # ----- # ----- # ----- # ----- # ----- # ----- # ----- # ----- #

$extIconPath = 'EXT:solr/Resources/Public/Images/Icons/';
if (TYPO3_MODE === 'BE') {
    $modulePrefix = 'extensions-solr-module';
    $bitmapProvider = \TYPO3\CMS\Core\Imaging\IconProvider\BitmapIconProvider::class;
    $svgProvider = \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class;

        // register all module icons with extensions-solr-module-modulename
    $iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class);
    $iconRegistry->registerIcon($modulePrefix . '-administration', $svgProvider,
        ['source' => $extIconPath . 'ModuleAdministration.svg']);
    $iconRegistry->registerIcon($modulePrefix . '-overview', $bitmapProvider,
        ['source' => $extIconPath . 'Search.png']);
    $iconRegistry->registerIcon($modulePrefix . '-indexqueue', $bitmapProvider,
        ['source' => $extIconPath . 'IndexQueue.png']);
    $iconRegistry->registerIcon($modulePrefix . '-indexmaintenance', $bitmapProvider,
        ['source' => $extIconPath . 'IndexMaintenance.png']);
    $iconRegistry->registerIcon($modulePrefix . '-indexfields', $bitmapProvider,
        ['source' => $extIconPath . 'IndexFields.png']);
    $iconRegistry->registerIcon($modulePrefix . '-stopwords', $bitmapProvider,
        ['source' => $extIconPath . 'StopWords.png']);
    $iconRegistry->registerIcon($modulePrefix . '-synonyms', $bitmapProvider,
        ['source' => $extIconPath . 'Synonyms.png']);
    $iconRegistry->registerIcon($modulePrefix . '-searchstatistics', $bitmapProvider,
        ['source' => $extIconPath . 'SearchStatistics.png']);
    $iconRegistry->registerIcon($modulePrefix . '-initsolrconnections', $svgProvider,
        ['source' => $extIconPath . 'InitSolrConnections.svg']);

    // register plugin icon
    $iconRegistry->registerIcon('extensions-solr-plugin-contentelement', $svgProvider,
        ['source' => $extIconPath . 'ContentElement.svg']);
}

if (TYPO3_MODE == 'BE') {
    \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule(
        'ApacheSolrForTypo3.' . $_EXTKEY,
        'tools',
        'administration',
        '',
        [
            // An array holding the controller-action-combinations that are accessible
            'Administration' => 'index,setSite,setCore,noSiteAvailable'
        ],
        [
            'access' => 'admin',
            'icon' => 'EXT:solr/Resources/Public/Images/Icons/ModuleAdministration.svg',
            'labels' => 'LLL:EXT:' . $_EXTKEY . '/Resources/Private/Language/locallang.xlf',
        ]
    );

    ApacheSolrForTypo3\Solr\Backend\SolrModule\AdministrationModuleManager::registerModule(
        'ApacheSolrForTypo3.' . $_EXTKEY,
        'Overview',
        ['index']
    );

    ApacheSolrForTypo3\Solr\Backend\SolrModule\AdministrationModuleManager::registerModule(
        'ApacheSolrForTypo3.' . $_EXTKEY,
        'IndexQueue',
        ['index,initializeIndexQueue,resetLogErrors,clearIndexQueue']
    );

    ApacheSolrForTypo3\Solr\Backend\SolrModule\AdministrationModuleManager::registerModule(
        'ApacheSolrForTypo3.' . $_EXTKEY,
        'IndexMaintenance',
        ['index,cleanUpIndex,emptyIndex,reloadIndexConfiguration']
    );

    ApacheSolrForTypo3\Solr\Backend\SolrModule\AdministrationModuleManager::registerModule(
        'ApacheSolrForTypo3.' . $_EXTKEY,
        'IndexFields',
        ['index']
    );

    ApacheSolrForTypo3\Solr\Backend\SolrModule\AdministrationModuleManager::registerModule(
        'ApacheSolrForTypo3.' . $_EXTKEY,
        'SearchStatistics',
        ['index']
    );

    ApacheSolrForTypo3\Solr\Backend\SolrModule\AdministrationModuleManager::registerModule(
        'ApacheSolrForTypo3.' . $_EXTKEY,
        'StopWords',
        ['index,saveStopWords']
    );

    ApacheSolrForTypo3\Solr\Backend\SolrModule\AdministrationModuleManager::registerModule(
        'ApacheSolrForTypo3.' . $_EXTKEY,
        'Synonyms',
        ['index,addSynonyms,deleteSynonyms']
    );

    // registering reports
    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['reports']['tx_reports']['status']['providers']['solr'] = [
        \ApacheSolrForTypo3\Solr\Report\SchemaStatus::class,
        \ApacheSolrForTypo3\Solr\Report\SolrConfigStatus::class,
        \ApacheSolrForTypo3\Solr\Report\SolrConfigurationStatus::class,
        \ApacheSolrForTypo3\Solr\Report\SolrStatus::class,
        \ApacheSolrForTypo3\Solr\Report\SolrVersionStatus::class,
        \ApacheSolrForTypo3\Solr\Report\AccessFilterPluginInstalledStatus::class,
        \ApacheSolrForTypo3\Solr\Report\AllowUrlFOpenStatus::class,
        \ApacheSolrForTypo3\Solr\Report\FilterVarStatus::class
    ];

    // Index Inspector
    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::insertModuleFunction(
        'web_info',
        \ApacheSolrForTypo3\Solr\Backend\IndexInspector\IndexInspector::class,
        null,
        'LLL:EXT:solr/Resources/Private/Language/locallang.xlf:module_indexinspector'
    );

    // register Clear Cache Menu hook
    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['additionalBackendItems']['cacheActions']['clearSolrConnectionCache'] = \ApacheSolrForTypo3\Solr\ConnectionManager::class;
}
if ((TYPO3_MODE === 'BE') || (TYPO3_MODE === 'FE' && isset($_POST['TSFE_EDIT']))) {
    // the order of registering the garbage collector and the record monitor is important!
    // for certain scenarios items must be removed by GC first, and then be re-added to to Index Queue

    // hooking into TCE Main to monitor record updates that may require deleting documents from the index
    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'][] = \ApacheSolrForTypo3\Solr\GarbageCollector::class;
    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = \ApacheSolrForTypo3\Solr\GarbageCollector::class;

    // hooking into TCE Main to monitor record updates that may require reindexing by the index queue
    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'][] = \ApacheSolrForTypo3\Solr\IndexQueue\RecordMonitor::class;
    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = \ApacheSolrForTypo3\Solr\IndexQueue\RecordMonitor::class;
}

# ----- # ----- # ----- # ----- # ----- # ----- # ----- # ----- # ----- #

// register click menu item to initialize the Solr connections for a single site
// visible for admin users only
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addUserTSConfig('
[adminUser = 1]
options.contextMenu.table.pages.items.850 = ITEM
options.contextMenu.table.pages.items.850 {
	name = Tx_Solr_initializeSolrConnections
	label = Initialize Solr Connections
	iconName = extensions-solr-module-initsolrconnections
	displayCondition = getRecord|is_siteroot = 1
	callbackAction = initializeSolrConnections
}

options.contextMenu.table.pages.items.851 = DIVIDER
[global]
');

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerExtDirectComponent(
    'TYPO3.Solr.ContextMenuActionController',
    \ApacheSolrForTypo3\Solr\ContextMenuActionController::class,
    'web',
    'admin'
);

// include JS in backend
$GLOBALS['TYPO3_CONF_VARS']['typo3/backend.php']['additionalBackendItems']['Solr.ContextMenuInitializeSolrConnectionsAction'] = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('solr') . 'Classes/BackendItem/ContextMenuActionJavascriptRegistration.php';

# ----- # ----- # ----- # ----- # ----- # ----- # ----- # ----- # ----- #

// replace the built-in search content element
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
    '*',
    'FILE:EXT:' . $_EXTKEY . '/Configuration/FlexForms/Results.xml',
    'search'
);

$TCA['tt_content']['types']['search']['showitem'] =
    '--palette--;LLL:EXT:cms/locallang_ttc.xml:palette.general;general,
	--palette--;LLL:EXT:cms/locallang_ttc.xml:palette.header;header,
	--div--;LLL:EXT:cms/locallang_ttc.xml:tabs.plugin,
		pi_flexform;;;;1-1-1,
	--div--;LLL:EXT:cms/locallang_ttc.xml:tabs.access,
		--palette--;LLL:EXT:cms/locallang_ttc.xml:palette.visibility;visibility,
		--palette--;LLL:EXT:cms/locallang_ttc.xml:palette.access;access,
	--div--;LLL:EXT:cms/locallang_ttc.xml:tabs.appearance,
		--palette--;LLL:EXT:cms/locallang_ttc.xml:palette.frames;frames,
	--div--;LLL:EXT:cms/locallang_ttc.xml:tabs.behaviour,
	--div--;LLL:EXT:cms/locallang_ttc.xml:tabs.extended';


@@ -5,12 +5,16 @@ declare(strict_types=1);
 namespace Tests\App\Functional\Controller;

 use App\DataFixtures\Demo\ProductDataFixture;
+use Faker\Provider\Text;
 use Shopsys\FrameworkBundle\Component\Domain\Domain;
-use Tests\App\Test\TransactionFunctionalTestCase;
+use Tests\App\Test\FunctionalTestCase;
+use Zalas\Injector\PHPUnit\Symfony\TestCase\SymfonyTestContainer;

-class ProductRenameRedirectPreviousUrlTest extends TransactionFunctionalTestCase
+class ProductRenameRedirectPreviousUrlTest extends FunctionalTestCase
 {
-    private const TESTED_PRODUCT_ID = 1;
+    use SymfonyTestContainer;
+
+    private const TESTED_PRODUCT_ID = 100;

     /**
      * @var \Shopsys\FrameworkBundle\Model\Product\ProductDataFactoryInterface

<?php

declare(strict_types=1);

namespace Tests\App\Functional\Controller;

use App\DataFixtures\Demo\ProductDataFixture;
use Shopsys\FrameworkBundle\Component\Domain\Domain;
use Tests\App\Test\TransactionFunctionalTestCase;

class ProductRenameRedirectPreviousUrlTest extends TransactionFunctionalTestCase
{
    private const TESTED_PRODUCT_ID = 1;

    /**
     * @var \Shopsys\FrameworkBundle\Model\Product\ProductDataFactoryInterface
     * @inject
     */
    private $productDataFactory;

    /**
     * @var \Shopsys\FrameworkBundle\Model\Product\ProductFacade
     * @inject
     */
    private $productFacade;

    /**
     * @var \Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlFacade
     * @inject
     */
    private $friendlyUrlFacade;

    public function testPreviousUrlRedirect(): void
    {
        $product = $this->getReference(ProductDataFixture::PRODUCT_PREFIX . self::TESTED_PRODUCT_ID);

        $previousFriendlyUrlSlug = $this->friendlyUrlFacade->findMainFriendlyUrl(Domain::FIRST_DOMAIN_ID, 'front_product_detail', self::TESTED_PRODUCT_ID)->getSlug();

        /** @var \App\Model\Product\Product $product */
        $productData = $this->productDataFactory->createFromProduct($product);
        $productData->name[$this->domain->getDomainConfigById(Domain::FIRST_DOMAIN_ID)->getLocale()] = 'rename';

        $this->productFacade->edit(self::TESTED_PRODUCT_ID, $productData);

        $client = $this->getClient();
        $client->request('GET', '/' . $previousFriendlyUrlSlug);

        // Should be 301 (moved permanently), because old product urls should be permanently redirected
        $this->assertEquals(301, $client->getResponse()->getStatusCode());
    }
}

Any particular reason to change TransactionalFunctionalTestCase to FunctionalTestCase and injecting entityManager? If so, it would be nice to explain it in commit message


@@ -73,6 +73,10 @@ class CarouselLoop extends Image
     {
         /** @var \Carousel\Model\Carousel $carousel */
         foreach ($loopResult->getResultDataCollection() as $carousel) {
+            if (!file_exists($carousel->getUploadDir() . DS . $carousel->getFile())) {
+                continue;
+            }
+
             $loopResultRow = new LoopResultRow($carousel);

             $event = new ImageEvent();

<?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 Carousel\Loop;

use Carousel\Model\CarouselQuery;
use Propel\Runtime\ActiveQuery\Criteria;
use Thelia\Core\Event\Image\ImageEvent;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Core\Template\Element\LoopResult;
use Thelia\Core\Template\Element\LoopResultRow;
use Thelia\Core\Template\Loop\Argument\Argument;
use Thelia\Core\Template\Loop\Argument\ArgumentCollection;
use Thelia\Core\Template\Loop\Image;
use Thelia\Type\EnumListType;
use Thelia\Type\EnumType;
use Thelia\Type\TypeCollection;

/**
 * Class CarouselLoop
 * @package Carousel\Loop
 * @author manuel raynaud <mraynaud@openstudio.fr>
 */
class CarouselLoop extends Image
{

    /**
     * @inheritdoc
     */
    protected function getArgDefinitions()
    {
        return new ArgumentCollection(
            Argument::createIntTypeArgument('width'),
            Argument::createIntTypeArgument('height'),
            Argument::createIntTypeArgument('rotation', 0),
            Argument::createAnyTypeArgument('background_color'),
            Argument::createIntTypeArgument('quality'),
            new Argument(
                'resize_mode',
                new TypeCollection(
                    new EnumType(array('crop', 'borders', 'none'))
                ),
                'none'
            ),
            new Argument(
                'order',
                new TypeCollection(
                    new EnumListType(array('alpha', 'alpha-reverse', 'manual', 'manual-reverse', 'random'))
                ),
                'manual'
            ),
            Argument::createAnyTypeArgument('effects'),
            Argument::createBooleanTypeArgument('allow_zoom', false)
        );
    }

    /**
     * @param LoopResult $loopResult
     *
     * @return LoopResult
     */
    public function parseResults(LoopResult $loopResult)
    {
        /** @var \Carousel\Model\Carousel $carousel */
        foreach ($loopResult->getResultDataCollection() as $carousel) {
            $loopResultRow = new LoopResultRow($carousel);

            $event = new ImageEvent();
            $event->setSourceFilepath($carousel->getUploadDir() . DS . $carousel->getFile())
                ->setCacheSubdirectory('carousel');

            switch ($this->getResizeMode()) {
                case 'crop':
                    $resize_mode = \Thelia\Action\Image::EXACT_RATIO_WITH_CROP;
                    break;

                case 'borders':
                    $resize_mode = \Thelia\Action\Image::EXACT_RATIO_WITH_BORDERS;
                    break;

                case 'none':
                default:
                    $resize_mode = \Thelia\Action\Image::KEEP_IMAGE_RATIO;

            }

            // Prepare tranformations
            $width = $this->getWidth();
            $height = $this->getHeight();
            $rotation = $this->getRotation();
            $background_color = $this->getBackgroundColor();
            $quality = $this->getQuality();
            $effects = $this->getEffects();

            if (!is_null($width)) {
                $event->setWidth($width);
            }
            if (!is_null($height)) {
                $event->setHeight($height);
            }
            $event->setResizeMode($resize_mode);
            if (!is_null($rotation)) {
                $event->setRotation($rotation);
            }
            if (!is_null($background_color)) {
                $event->setBackgroundColor($background_color);
            }
            if (!is_null($quality)) {
                $event->setQuality($quality);
            }
            if (!is_null($effects)) {
                $event->setEffects($effects);
            }

            $event->setAllowZoom($this->getAllowZoom());

            // Dispatch image processing event
            $this->dispatcher->dispatch(TheliaEvents::IMAGE_PROCESS, $event);

            $loopResultRow
                ->set('ID', $carousel->getId())
                ->set("LOCALE", $this->locale)
                ->set("IMAGE_URL", $event->getFileUrl())
                ->set("ORIGINAL_IMAGE_URL", $event->getOriginalFileUrl())
                ->set("IMAGE_PATH", $event->getCacheFilepath())
                ->set("ORIGINAL_IMAGE_PATH", $event->getSourceFilepath())
                ->set("TITLE", $carousel->getVirtualColumn('i18n_TITLE'))
                ->set("CHAPO", $carousel->getVirtualColumn('i18n_CHAPO'))
                ->set("DESCRIPTION", $carousel->getVirtualColumn('i18n_DESCRIPTION'))
                ->set("POSTSCRIPTUM", $carousel->getVirtualColumn('i18n_POSTSCRIPTUM'))
                ->set("ALT", $carousel->getVirtualColumn('i18n_ALT'))
                ->set("URL", $carousel->getUrl())
                ->set('POSITION', $carousel->getPosition())
            ;

            $loopResult->addRow($loopResultRow);
        }

        return $loopResult;
    }

    /**
     * this method returns a Propel ModelCriteria
     *
     * @return \Propel\Runtime\ActiveQuery\ModelCriteria
     */
    public function buildModelCriteria()
    {
        $search = CarouselQuery::create();

        $this->configureI18nProcessing($search, [ 'ALT', 'TITLE', 'CHAPO', 'DESCRIPTION', 'POSTSCRIPTUM' ]);

        $orders  = $this->getOrder();

        // Results ordering
        foreach ($orders as $order) {
            switch ($order) {
                case "alpha":
                    $search->addAscendingOrderByColumn('i18n_TITLE');
                    break;
                case "alpha-reverse":
                    $search->addDescendingOrderByColumn('i18n_TITLE');
                    break;
                case "manual-reverse":
                    $search->orderByPosition(Criteria::DESC);
                    break;
                case "manual":
                    $search->orderByPosition(Criteria::ASC);
                    break;
                case "random":
                    $search->clearOrderByColumns();
                    $search->addAscendingOrderByColumn('RAND()');
                    break(2);
                    break;
            }
        }

        return $search;
    }
}

Adding a warning or an error in the Thelia log would be a nice idea.


@@ -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`)