diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index aea19c986e9..48d9aaa0676 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -261,7 +261,7 @@ jobs: - name: Check standards run: | docker compose exec -T php-fpm touch ./project-base/app/DEVELOPMENT - docker compose exec -T php-fpm php phing -D production.confirm.action=y composer-dev standards + docker compose exec -T php-fpm php phing -D production.confirm.action=y composer-dev frontend-api-generate-new-keys standards - name: Check GraphQl schema run: docker compose exec -T php-fpm project-base/app/check-schema.sh standards-storefront: diff --git a/CHANGELOG-15.0.md b/CHANGELOG-15.0.md index c97e15cdd27..72ba9e913ea 100644 --- a/CHANGELOG-15.0.md +++ b/CHANGELOG-15.0.md @@ -26,6 +26,7 @@ There is a list of all the repositories maintained by the monorepo: - [shopsys/frontend-api](https://github.com/shopsys/frontend-api) - [shopsys/php-image](https://github.com/shopsys/php-image) - [shopsys/luigis-box](https://github.com/shopsys/luigis-box) +- [shopsys/administration](https://github.com/shopsys/administration) Packages are formatted by release version. You can see all the changes done to the package that you carry about with this tree. diff --git a/composer.json b/composer.json index 7da9fef7b78..861dc84c603 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "psr-4": { "App\\": "project-base/app/src/", "Overblog\\GraphQLBundle\\__DEFINITIONS__\\": "project-base/app/var/overblogCompiledClasses", + "Shopsys\\Administration\\": "packages/administration/src/", "Shopsys\\CodingStandards\\": "packages/coding-standards/src/", "Shopsys\\FormTypesBundle\\": "packages/form-types-bundle/src/", "Shopsys\\FrontendApiBundle\\": "packages/frontend-api/src/", @@ -39,6 +40,7 @@ "autoload-dev": { "psr-4": { "Tests\\": "project-base/app/tests/", + "Tests\\Administration\\": "packages/administration/tests/", "Tests\\CodingStandards\\": "packages/coding-standards/tests/", "Tests\\FrameworkBundle\\": "packages/framework/tests/", "Tests\\FrontendApiBundle\\": "packages/frontend-api/tests/", @@ -84,6 +86,7 @@ "barryvdh/elfinder-flysystem-driver": "^0.4.3", "commerceguys/intl": "^1.0.0", "composer/composer": "^2.2.12", + "cweagans/composer-patches": "^1.7", "defuse/php-encryption": "^2.2.1", "doctrine/annotations": "^1.6.0", "doctrine/cache": "^1.7", @@ -96,7 +99,7 @@ "doctrine/doctrine-migrations-bundle": "^3.2.2", "doctrine/migrations": "^3.4.1", "doctrine/orm": "^2.11.2", - "doctrine/persistence": "^2.4", + "doctrine/persistence": "^3.3", "elasticsearch/elasticsearch": "^7.6.1", "enlightn/security-checker": "^1.3", "fakerphp/faker": "^1.19.0", @@ -127,12 +130,12 @@ "overblog/dataloader-bundle": "^0.6.0", "overblog/graphiql-bundle": "^0.3", "overblog/graphql-bundle": "1.3.2", - "phing/phing": "3.0.0-rc6", + "phing/phing": "^3.0.0", "phpdocumentor/reflection-docblock": "^5.3.0", "presta/sitemap-bundle": "^3.3", "prezent/doctrine-translatable": "^3.3.0", "prezent/doctrine-translatable-bundle": "^1.4", - "psr/log": "^1.0", + "psr/log": "^2.0", "ramsey/uuid": "^4.3.1", "roave/better-reflection": "^6.0", "scheb/2fa-bundle": "^5.7", @@ -145,7 +148,9 @@ "shopsys/jsformvalidator-bundle": "^1.7.0", "shopsys/ordered-form": "^5.2", "snc/redis-bundle": "^4.4.1", + "sonata-project/admin-bundle": "^4.30", "spatie/opening-hours": "^3.0", + "sonata-project/doctrine-orm-admin-bundle": "^4.9", "stof/doctrine-extensions-bundle": "^1.3.0", "symfony-cmf/routing": "^2.0.3", "symfony-cmf/routing-bundle": "^2.0.3", @@ -241,7 +246,8 @@ "ocramius/package-versions": true, "symfony/flex": true, "php-http/discovery": true, - "symfony/runtime": true + "symfony/runtime": true, + "cweagans/composer-patches": true } }, "extra": { @@ -257,9 +263,17 @@ }, "runtime": { "dotenv_path": "project-base/app/.env" + }, + "enable-patching": true, + "patches": { + "composer-exit-on-patch-failure": true, + "twig/twig": { + "Hotfix for issue https://github.com/sonata-project/SonataAdminBundle/issues/8181": "https://github.com/TomasLudvik/Twig/commit/4e07748eb6d427688221e2c7b6dc8872c6d9065c.patch" + } } }, "replace": { + "shopsys/administration": "self.version", "shopsys/article-feed-luigis-box": "self.version", "shopsys/brand-feed-luigis-box": "self.version", "shopsys/category-feed-luigis-box": "self.version", diff --git a/docs/contributing/guidelines-for-writing-upgrade.md b/docs/contributing/guidelines-for-writing-upgrade.md index 938a1a6ef91..d29a655c94e 100644 --- a/docs/contributing/guidelines-for-writing-upgrade.md +++ b/docs/contributing/guidelines-for-writing-upgrade.md @@ -51,6 +51,7 @@ Each upgrade file must have a link to the main UPGRADE.md file with general info - shopsys/product-feed-luigis-box - shopsys/article-feed-luigis-box - shopsys/luigis-box +- shopsys/administration Each section must contain instructions relevant only to the package they cover, and the sections have to be ordered as they are in the list above. diff --git a/docs/introduction/monorepo.md b/docs/introduction/monorepo.md index 67573bbd11d..e2c9b3e1060 100644 --- a/docs/introduction/monorepo.md +++ b/docs/introduction/monorepo.md @@ -50,6 +50,7 @@ If you are interested, you can read more about the monorepo approach here - http - [shopsys/s3-bridge](https://github.com/shopsys/s3-bridge) - [shopsys/php-image](https://github.com/shopsys/php-image) - [shopsys/luigis-box](https://github.com/shopsys/luigis-box) +- [shopsys/administration](https://github.com/shopsys/administration) !!! note diff --git a/ecs.php b/ecs.php index a0a86d9301c..6485d4b204b 100644 --- a/ecs.php +++ b/ecs.php @@ -7,6 +7,7 @@ use Shopsys\CodingStandards\Helper\CyclomaticComplexitySniffSetting; use Shopsys\CodingStandards\Sniffs\ConstantVisibilityRequiredSniff; use Shopsys\CodingStandards\Sniffs\ForceLateStaticBindingForProtectedConstantsSniff; +use Shopsys\CodingStandards\Sniffs\ObjectIsCreatedByFactorySniff; use SlevomatCodingStandard\Sniffs\Functions\FunctionLengthSniff; use Symplify\EasyCodingStandard\Config\ECSConfig; @@ -33,6 +34,11 @@ ], FunctionLengthSniff::class => [ __DIR__ . '/utils/releaser/src/ReleaseWorker/Release/CreateAndPushGitTagsExceptProjectBaseReleaseWorker.php', + __DIR__ . '/packages/administration/src/Controller/CRUDController.php', + __DIR__ . '/packages/administration/src/**/*Admin.php', + ], + ObjectIsCreatedByFactorySniff::class => [ + __DIR__ . '/packages/administration/src/Component/FieldDescription/FieldDescriptionFactory.php', ], ], ); diff --git a/packages/administration/.github/workflows/run-checks-tests.yaml b/packages/administration/.github/workflows/run-checks-tests.yaml new file mode 100644 index 00000000000..b58d1f7cb0b --- /dev/null +++ b/packages/administration/.github/workflows/run-checks-tests.yaml @@ -0,0 +1,28 @@ +on: [push] +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true +name: "Checks and tests" +jobs: + checks-and-tests: + name: Run checks and tests + runs-on: ubuntu-22.04 + steps: + - name: GIT checkout branch - ${{ github.ref }} + uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + - name: Install PHP, extensions and tools + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + extensions: bcmath, gd, intl, pdo_pgsql, redis, pgsql, zip + tools: composer + - name: Install Composer dependencies + run: composer install --optimize-autoloader --no-interaction + - name: Run parallel-lint + run: php vendor/bin/parallel-lint ./src ./tests + - name: Run Easy Coding Standards + run: php vendor/bin/ecs check --verbose ./src ./tests + - name: Run PHPUnit + run: php vendor/bin/phpunit tests diff --git a/packages/administration/.gitignore b/packages/administration/.gitignore new file mode 100644 index 00000000000..d2f57af5cde --- /dev/null +++ b/packages/administration/.gitignore @@ -0,0 +1,5 @@ +/.idea +/.php_cs.cache +/composer.lock +/vendor +/.phpunit.result.cache diff --git a/packages/administration/CONTRIBUTING.md b/packages/administration/CONTRIBUTING.md new file mode 100644 index 00000000000..5bebb043684 --- /dev/null +++ b/packages/administration/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# Contributing + +Thank you for your contributions to Shopsys Admin bundle package. +Together, we are making Shopsys Platform better. + +This repository is READ-ONLY. +If you want to [report issues](https://github.com/shopsys/shopsys/issues/new) and/or send [pull requests](https://github.com/shopsys/shopsys/compare), +please use the main [Shopsys repository](https://github.com/shopsys/shopsys). + +Please check our [Contribution Guide](https://docs.shopsys.com/en/latest/contributing/) before contributing. diff --git a/packages/administration/LICENSE b/packages/administration/LICENSE new file mode 100644 index 00000000000..36842926336 --- /dev/null +++ b/packages/administration/LICENSE @@ -0,0 +1,14 @@ +MIT License + +Copyright (c) 2017-2023 Shopsys s.r.o., http://www.shopsys.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/administration/README.md b/packages/administration/README.md new file mode 100644 index 00000000000..41bd364b23e --- /dev/null +++ b/packages/administration/README.md @@ -0,0 +1,39 @@ +# Shopsys Administration bundle + +[![Downloads](https://img.shields.io/packagist/dt/shopsys/administration.svg)](https://packagist.org/packages/shopsys/administration) + +Bundle for [Shopsys Platform](https://www.shopsys.com/shopsys-platform/) responsible for generating administration. + +This repository is maintained by [shopsys/shopsys] monorepo, information about changes is in [monorepo CHANGELOG.md](https://github.com/shopsys/shopsys/blob/master/CHANGELOG.md). + +## Installation + +The plugin is a Symfony bundle and is installed in the same way: + +### Download + +Download the package using [Composer](https://getcomposer.org/): + +``` +composer require shopsys/administration +``` + +## Contributing + +Thank you for your contributions to Shopsys Administration bundle package. +Together, we are making Shopsys Platform better. + +This repository is READ-ONLY. +If you want to [report issues](https://github.com/shopsys/shopsys/issues/new) and/or send [pull requests](https://github.com/shopsys/shopsys/compare), +please use the main [Shopsys repository](https://github.com/shopsys/shopsys). + +Please check our [Contribution Guide](https://docs.shopsys.com/en/latest/contributing/) before contributing. + +## Support + +What to do when you are in trouble or need some help? +The best way is to join our [Slack](https://join.slack.com/t/shopsysframework/shared_invite/zt-11wx9au4g-e5pXei73UJydHRQ7nVApAQ). + +If you want to [report issues](https://github.com/shopsys/shopsys/issues/new), please use the main [Shopsys repository](https://github.com/shopsys/shopsys). + +[shopsys/shopsys]: (https://github.com/shopsys/shopsys) diff --git a/packages/administration/composer.json b/packages/administration/composer.json new file mode 100644 index 00000000000..55b15a69eed --- /dev/null +++ b/packages/administration/composer.json @@ -0,0 +1,51 @@ +{ + "name": "shopsys/administration", + "type": "library", + "description": "Shopsys Platform administration", + "keywords": ["administration", "Shopsys Platform", "SSFW", "SSP"], + "license": "MIT", + "authors": [ + { + "name": "Shopsys", + "homepage": "https://www.shopsys.com/" + } + ], + "autoload": { + "psr-4": { + "Shopsys\\Administration\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\Administration\\": "tests/" + } + }, + "require": { + "php": "^8.3", + "ext-json": "*", + "cweagans/composer-patches": "^1.7", + "sonata-project/admin-bundle": "^4.30", + "sonata-project/doctrine-orm-admin-bundle": "^4.9", + "twig/twig": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.20", + "shopsys/coding-standards": "15.0.x-dev" + }, + "config": { + "allow-plugins": { + "symfony/flex": true, + "dealerdirect/phpcodesniffer-composer-installer": true, + "cweagans/composer-patches": true + } + }, + "extra": { + "enable-patching": true, + "patches": { + "composer-exit-on-patch-failure": true, + "twig/twig": { + "Hotfix for issue https://github.com/sonata-project/SonataAdminBundle/issues/8181": "https://github.com/TomasLudvik/Twig/commit/4e07748eb6d427688221e2c7b6dc8872c6d9065c.patch" + } + } + } +} diff --git a/packages/administration/ecs.php b/packages/administration/ecs.php new file mode 100644 index 00000000000..cdd5bae975e --- /dev/null +++ b/packages/administration/ecs.php @@ -0,0 +1,31 @@ +import(__DIR__ . '/vendor/shopsys/coding-standards/ecs.php', null, true); + + $ecsConfig->skip( + [ + ObjectIsCreatedByFactorySniff::class => [ + __DIR__ . '/packages/administration/src/Component/FieldDescription/FieldDescriptionFactory.php', + ], + FunctionLengthSniff::class => [ + __DIR__ . '/packages/administration/src/Controller/CRUDController.php', + __DIR__ . '/packages/administration/src/**/*Admin.php', + ], + ], + ); +}; diff --git a/packages/administration/phpunit.xml b/packages/administration/phpunit.xml new file mode 100644 index 00000000000..9bec070219b --- /dev/null +++ b/packages/administration/phpunit.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/packages/administration/src/Component/Admin/AbstractAdmin.php b/packages/administration/src/Component/Admin/AbstractAdmin.php new file mode 100644 index 00000000000..6f02d211ff8 --- /dev/null +++ b/packages/administration/src/Component/Admin/AbstractAdmin.php @@ -0,0 +1,84 @@ +getModelManager(); + $dataObject = $modelManager->createDataObject(); + + $this->setModelClass(ClassUtils::getClass($dataObject)); + + return $dataObject; + } + + /** + * @param object $object + * @return object + */ + public function generateDataObject(object $object): object + { + /** @var \Shopsys\Administration\Component\Admin\AbstractDtoManager $modelManager */ + $modelManager = $this->getModelManager(); + + return $modelManager->buildDataObjectForEdit($object); + } + + /** + * @param object $object + * @return string + */ + public function toString(object $object): string + { + if ($object instanceof AdminIdentifierInterface && $object->getId() !== null) { + /** @var \Shopsys\Administration\Component\Admin\AbstractDtoManager $modelManager */ + $modelManager = $this->getModelManager(); + $realClasss = $modelManager->getRealClass($object, false); + $object = $modelManager->find($realClasss, $object->getId()); + } + + return parent::toString($object); + } + + /** + * @return string + */ + protected function getSubjectClassName(): string + { + /** @var \Shopsys\Administration\Component\Admin\AbstractDtoManager $modelManager */ + $modelManager = $this->getModelManager(); + $reflectionClass = ClassUtils::newReflectionClass($modelManager->getSubjectClass()); + + return mb_strtolower($reflectionClass->getShortName()); + } + + /** + * @param bool $isChildAdmin + * @return string + */ + protected function generateBaseRouteName(bool $isChildAdmin = false): string + { + return 'admin_' . 'new_' . $this->getSubjectClassName(); + } + + /** + * @param bool $isChildAdmin + * @return string + */ + protected function generateBaseRoutePattern(bool $isChildAdmin = false): string + { + return $this->getSubjectClassName(); + } +} diff --git a/packages/administration/src/Component/Admin/AbstractDtoManager.php b/packages/administration/src/Component/Admin/AbstractDtoManager.php new file mode 100644 index 00000000000..3ab1de7142e --- /dev/null +++ b/packages/administration/src/Component/Admin/AbstractDtoManager.php @@ -0,0 +1,537 @@ +entityNameResolver->resolve($this->getSubjectClass()); + + if ($dataObject === true) { + return $entityName . 'Data'; + } + + return $entityName; + } + + /** + * @param object $object + */ + public function create(object $object): void + { + try { + $createdObject = $this->doCreate($object); + $object->id = $createdObject->getId(); + } catch (PDOException|Exception $exception) { + throw new ModelManagerException( + sprintf('Failed to create object: %s', ClassUtils::getClass($object)), + (int)$exception->getCode(), + $exception, + ); + } + } + + /** + * @param object $object + */ + public function update(object $object): void + { + try { + $this->doEdit($object); + } catch (PDOException|Exception $exception) { + throw new ModelManagerException( + sprintf('Failed to update object: %s', ClassUtils::getClass($object)), + (int)$exception->getCode(), + $exception, + ); + } + } + + /** + * @param object $object + */ + public function delete(object $object): void + { + try { + $this->doDelete($object); + } catch (PDOException|Exception $exception) { + throw new ModelManagerException( + sprintf('Failed to delete object: %s', ClassUtils::getClass($object)), + (int)$exception->getCode(), + $exception, + ); + } + } + + /** + * @param object $object + */ + public function getLockVersion(object $object) + { + $metadata = $this->getMetadata(ClassUtils::getClass($object)); + + if (!$metadata->isVersioned || !isset($metadata->reflFields[$metadata->versionField])) { + return null; + } + + return $metadata->reflFields[$metadata->versionField]->getValue($object); + } + + /** + * @param object $object + * @param int|null $expectedVersion + */ + public function lock(object $object, ?int $expectedVersion): void + { + $metadata = $this->getMetadata(ClassUtils::getClass($object)); + + if (!$metadata->isVersioned) { + return; + } + + try { + $entityManager = $this->getEntityManager($object); + $entityManager->lock($object, LockMode::OPTIMISTIC, $expectedVersion); + } catch (OptimisticLockException $exception) { + throw new LockException( + $exception->getMessage(), + $exception->getCode(), + $exception, + ); + } + } + + /** + * @param string $class + * @param int|string $id + * @phpstan-param class-string $class + * @phpstan-return T|null + * @return object|null + */ + public function find(string $class, $id): ?object + { + $values = array_combine($this->getIdentifierFieldNames($class), explode(self::ID_SEPARATOR, (string)$id)); + + return $this->getEntityManager($class)->getRepository($class)->find($values); + } + + /** + * @phpstan-param class-string $class + * @phpstan-return array + * @param string $class + * @param array $criteria + * @return array + */ + public function findBy(string $class, array $criteria = []): array + { + return $this->getEntityManager($class)->getRepository($class)->findBy($criteria); + } + + /** + * @phpstan-param class-string $class + * @phpstan-return T|null + * @param string $class + * @param array $criteria + * @return object|null + */ + public function findOneBy(string $class, array $criteria = []): ?object + { + return $this->getEntityManager($class)->getRepository($class)->findOneBy($criteria); + } + + /** + * @param string|object $class + * @phpstan-param class-string|object $class + * @return \Doctrine\ORM\EntityManagerInterface + */ + protected function getEntityManager($class): EntityManagerInterface + { + if (is_object($class)) { + $class = get_class($class); + } + + if (!isset($this->cache[$class])) { + $em = $this->registry->getManagerForClass($class); + + if (!$em instanceof EntityManagerInterface) { + throw new RuntimeException(sprintf('No entity manager defined for class %s', $class)); + } + + $this->cache[$class] = $em; + } + + return $this->cache[$class]; + } + + /** + * @param string $class + * @param string $alias + * @return \Sonata\AdminBundle\Datagrid\ProxyQueryInterface + */ + public function createQuery(string $class, string $alias = 'o'): BaseProxyQueryInterface + { + $repository = $this->getEntityManager($class)->getRepository($class); + /** @phpstan-var \Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery $proxyQuery */ + $proxyQuery = new ProxyQuery($repository->createQueryBuilder($alias)); + + return $proxyQuery; + } + + /** + * @param object $query + * @return bool + */ + public function supportsQuery(object $query): bool + { + return $query instanceof ProxyQuery || $query instanceof AbstractQuery || $query instanceof QueryBuilder; + } + + /** + * @param object $query + */ + public function executeQuery(object $query) + { + if ($query instanceof QueryBuilder) { + return $query->getQuery()->execute(); + } + + if ($query instanceof AbstractQuery) { + return $query->execute(); + } + + if ($query instanceof ProxyQuery) { + /** @phpstan-var \Doctrine\ORM\Tools\Pagination\Paginator $results */ + $results = $query->execute(); + + return $results; + } + + throw new InvalidArgumentException(sprintf( + 'Argument 1 passed to %s() must be an instance of %s, %s, or %s', + __METHOD__, + QueryBuilder::class, + AbstractQuery::class, + ProxyQuery::class, + )); + } + + /** + * @param object $model + * @return array + */ + public function getIdentifierValues(object $model): array + { + $class = ClassUtils::getClass($model); + $metadata = $this->getMetadata($class); + $platform = $this->getEntityManager($class)->getConnection()->getDatabasePlatform(); + + $identifiers = []; + + foreach ($metadata->getIdentifierValues($model) as $name => $value) { + if (!is_object($value)) { + $identifiers[] = $value; + + continue; + } + + $fieldType = $metadata->getTypeOfField($name); + + if ($fieldType !== null && Type::hasType($fieldType)) { + $identifiers[] = $this->getValueFromType($value, Type::getType($fieldType), $fieldType, $platform); + + continue; + } + + $identifierMetadata = $this->getMetadata(ClassUtils::getClass($value)); + + foreach ($identifierMetadata->getIdentifierValues($value) as $identifierValue) { + $identifiers[] = $identifierValue; + } + } + + return $identifiers; + } + + /** + * @param string $class + * @return array + */ + public function getIdentifierFieldNames(string $class): array + { + return $this->getMetadata($class)->getIdentifierFieldNames(); + } + + /** + * @param object $model + * @return string|null + */ + public function getNormalizedIdentifier(object $model): ?string + { + $values = [$model->getId()]; + + return implode(self::ID_SEPARATOR, $values); + } + + /** + * The ORM implementation does nothing special but you still should use + * this method when using the id in a URL to allow for future improvements. + * + * @param object $model + * @return string|null + */ + public function getUrlSafeIdentifier(object $model): ?string + { + return $this->getNormalizedIdentifier($model); + } + + /** + * @param string $class + * @param \Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQueryInterface $query + * @param array $idx + */ + public function addIdentifiersToQuery(string $class, BaseProxyQueryInterface $query, array $idx): void + { + if ($idx === []) { + throw new InvalidArgumentException(sprintf( + 'Array passed as argument 3 to "%s()" must not be empty.', + __METHOD__, + )); + } + + $fieldNames = $this->getIdentifierFieldNames($class); + $qb = $query->getQueryBuilder(); + + $prefix = uniqid('', true); + $sqls = []; + + foreach ($idx as $pos => $id) { + $ids = explode(self::ID_SEPARATOR, (string)$id); + + $ands = []; + + foreach ($fieldNames as $posName => $name) { + $parameterName = sprintf('field_%s_%s_%d', $prefix, $name, $pos); + $ands[] = sprintf('%s.%s = :%s', current($qb->getRootAliases()), $name, $parameterName); + $qb->setParameter($parameterName, $ids[$posName]); + } + + $sqls[] = implode(' AND ', $ands); + } + + $qb->andWhere(sprintf('( %s )', implode(' OR ', $sqls))); + } + + /** + * @param string $class + * @param \Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQueryInterface $query + */ + public function batchDelete(string $class, BaseProxyQueryInterface $query): void + { + if ($query->getQueryBuilder()->getDQLPart('join') !== []) { + $rootAlias = current($query->getQueryBuilder()->getRootAliases()); + + // Distinct is needed to iterate, even if group by is used + // @see https://github.com/doctrine/orm/issues/5868 + $query->getQueryBuilder()->distinct(); + $query->getQueryBuilder()->select($rootAlias); + } + + $entityManager = $this->getEntityManager($class); + $i = 0; + + try { + foreach ($query->getDoctrineQuery()->toIterable() as $object) { + $entityManager->remove($object); + + if (0 !== (++$i % 20)) { + continue; + } + + $entityManager->flush(); + $entityManager->clear(); + } + + $entityManager->flush(); + $entityManager->clear(); + } catch (PDOException|Exception $exception) { + throw new ModelManagerException( + sprintf('Failed to delete object: %s', $class), + (int)$exception->getCode(), + $exception, + ); + } + } + + /** + * @param string $class + * @return array + */ + public function getExportFields(string $class): array + { + return $this->getMetadata($class)->getFieldNames(); + } + + /** + * @param object $object + * @param array $array + */ + public function reverseTransform(object $object, array $array = []): void + { + $metadata = $this->getMetadata(get_class($object)); + + foreach ($array as $name => $value) { + $property = $this->getFieldName($metadata, $name); + $this->propertyAccessor->setValue($object, $property, $value); + } + } + + /** + * @phpstan-template TObject of object + * @phpstan-param class-string $class + * @phpstan-return \Doctrine\ORM\Mapping\ClassMetadata + * @param string $class + * @return \Doctrine\ORM\Mapping\ClassMetadata + */ + protected function getMetadata(string $class): ClassMetadata + { + return $this->getEntityManager($class)->getClassMetadata($class); + } + + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata + * @param string $name + * @return string + */ + protected function getFieldName(ClassMetadata $metadata, string $name): string + { + if (array_key_exists($name, $metadata->fieldMappings)) { + return $metadata->fieldMappings[$name]['fieldName']; + } + + if (array_key_exists($name, $metadata->associationMappings)) { + return $metadata->associationMappings[$name]['fieldName']; + } + + return $name; + } + + /** + * @param object $value + * @param \Doctrine\DBAL\Types\Type $type + * @param string $fieldType + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + * @return string + */ + protected function getValueFromType( + object $value, + Type $type, + string $fieldType, + AbstractPlatform $platform, + ): string { + if ($platform->hasDoctrineTypeMappingFor($fieldType) && + $platform->getDoctrineTypeMapping($fieldType) === 'binary' + ) { + return (string)$type->convertToPHPValue($value, $platform); + } + + // some libraries may have `toString()` implementation + if (is_callable([$value, 'toString'])) { + return $value->toString(); + } + + // final fallback to magic `__toString()` which may throw an exception in 7.4 + if (method_exists($value, '__toString')) { + return $value->__toString(); + } + + return (string)$type->convertToDatabaseValue($value, $platform); + } +} diff --git a/packages/administration/src/Component/FieldDescription/FieldDescriptionFactory.php b/packages/administration/src/Component/FieldDescription/FieldDescriptionFactory.php new file mode 100644 index 00000000000..953e0fd698a --- /dev/null +++ b/packages/administration/src/Component/FieldDescription/FieldDescriptionFactory.php @@ -0,0 +1,111 @@ +getParentMetadataForProperty($class, $name); + + return new FieldDescription( + $name, + $options, + $metadata->fieldMappings[$propertyName] ?? [], + $metadata->associationMappings[$propertyName] ?? [], + $parentAssociationMappings, + $propertyName, + ); + } + + /** + * @phpstan-param class-string $baseClass + * @phpstan-return array{\Doctrine\ORM\Mapping\ClassMetadata, string, mixed[]} + * @param string $baseClass + * @param string $propertyFullName + * @return array + */ + protected function getParentMetadataForProperty(string $baseClass, string $propertyFullName): array + { + $nameElements = explode('.', $propertyFullName); + $lastPropertyName = array_pop($nameElements); + $class = $baseClass; + $parentAssociationMappings = []; + + foreach ($nameElements as $nameElement) { + $metadata = $this->getMetadata($class); + + if (!isset($metadata->associationMappings[$nameElement])) { + break; + } + + $parentAssociationMappings[] = $metadata->associationMappings[$nameElement]; + $class = $metadata->getAssociationTargetClass($nameElement); + } + + $properties = array_slice($nameElements, count($parentAssociationMappings)); + $properties[] = $lastPropertyName; + + return [ + $this->getMetadata($class), + implode('.', $properties), + $parentAssociationMappings, + ]; + } + + /** + * @phpstan-template TObject of object + * @phpstan-param class-string $class + * @phpstan-return \Doctrine\ORM\Mapping\ClassMetadata + * @param string $class + * @return \Doctrine\ORM\Mapping\ClassMetadata + */ + protected function getMetadata(string $class): ClassMetadata + { + return $this->getEntityManager($class)->getClassMetadata($class); + } + + /** + * @param class-string $class + * @throw \UnexpectedValueException + * @return \Doctrine\ORM\EntityManagerInterface + */ + protected function getEntityManager(string $class): EntityManagerInterface + { + $em = $this->registry->getManagerForClass($class); + + if (!$em instanceof EntityManagerInterface) { + throw new UnexpectedValueException(sprintf('No entity manager defined for class "%s".', $class)); + } + + return $em; + } +} diff --git a/packages/administration/src/Component/Security/AdminIdentifierInterface.php b/packages/administration/src/Component/Security/AdminIdentifierInterface.php new file mode 100644 index 00000000000..fcbf9c2610a --- /dev/null +++ b/packages/administration/src/Component/Security/AdminIdentifierInterface.php @@ -0,0 +1,13 @@ +attributes->get('_route') === 'sonata_admin_login' && $request->isMethod('POST'); + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * @return array + */ + public function getCredentials(Request $request): array + { + $form = $this->formFactory->create(AdminLoginForm::class); + $form->handleRequest($request); + + $data = $form->getData(); + $request->getSession()->set( + Security::LAST_USERNAME, + $data['email'], + ); + + return $data; + } + + /** + * @param mixed $credentials + * @param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider + * @return \Symfony\Component\Security\Core\User\UserInterface + */ + public function getUser($credentials, UserProviderInterface $userProvider): UserInterface + { + return $userProvider->loadUserByUsername($credentials['email']); + } + + /** + * @param mixed $credentials + * @param \Symfony\Component\Security\Core\User\UserInterface $user + * @return bool + */ + public function checkCredentials($credentials, UserInterface $user): bool + { + return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * @param \Symfony\Component\Security\Core\Exception\AuthenticationException $exception + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): RedirectResponse + { + $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); + + return new RedirectResponse($this->router->generate('sonata_admin_login')); + } + + /** + * @return string + */ + protected function getLoginUrl(): string + { + return $this->router->generate('sonata_admin_login'); + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * @param \Symfony\Component\Security\Core\Authentication\Token\TokenInterface $token + * @param mixed $providerKey + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): RedirectResponse + { + return new RedirectResponse($this->router->generate('sonata_admin_dashboard')); + } +} diff --git a/packages/administration/src/Controller/AdminLoginController.php b/packages/administration/src/Controller/AdminLoginController.php new file mode 100644 index 00000000000..145e51837db --- /dev/null +++ b/packages/administration/src/Controller/AdminLoginController.php @@ -0,0 +1,45 @@ +createForm(AdminLoginForm::class, [ + 'email' => $this->authenticationUtils->getLastUsername(), + ]); + + return $this->render('@ShopsysAdministration/security/login.html.twig', [ + 'last_username' => $this->authenticationUtils->getLastUsername(), + 'form' => $form->createView(), + 'error' => $this->authenticationUtils->getLastAuthenticationError(), + ]); + } + + #[Route('/admin-new/logout', name: 'sonata_admin_logout')] + public function logoutAction(): void + { + // Left empty intentionally because this will be handled by Symfony. + } +} diff --git a/packages/administration/src/Controller/CRUDController.php b/packages/administration/src/Controller/CRUDController.php new file mode 100644 index 00000000000..515de1525f9 --- /dev/null +++ b/packages/administration/src/Controller/CRUDController.php @@ -0,0 +1,142 @@ +assertObjectExists($request, true); + assert($existingObject !== null); + + $this->checkParentChildAssociation($request, $existingObject); + + $this->admin->checkAccess('edit', $existingObject); + + $preResponse = $this->preEdit($request, $existingObject); + + if ($preResponse !== null) { + return $preResponse; + } + $this->admin->setSubject($existingObject); + $objectId = $this->admin->getNormalizedIdentifier($existingObject); + assert($objectId !== null); + + $form = $this->admin->getForm(); + + // TODO: custom one line + $existingObject = $this->admin->generateDataObject($existingObject); + + $form->setData($existingObject); + $form->handleRequest($request); + + if ($form->isSubmitted()) { + $isFormValid = $form->isValid(); + + // persist if the form was valid and if in preview mode the preview was approved + if ($isFormValid && (!$this->isInPreviewMode($request) || $this->isPreviewApproved($request))) { + /** @phpstan-var T $submittedObject */ + $submittedObject = $form->getData(); + + // TODO: custom one line + //$this->admin->setSubject($submittedObject); + + try { + $existingObject = $this->admin->update($submittedObject); + + if ($this->isXmlHttpRequest($request)) { + return $this->handleXmlHttpRequestSuccessResponse($request, $existingObject); + } + + $this->addFlash( + 'sonata_flash_success', + $this->trans( + 'flash_edit_success', + ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))], + 'SonataAdminBundle', + ), + ); + + // redirect to edit mode + return $this->redirectTo($request, $existingObject); + } catch (ModelManagerException $e) { + // NEXT_MAJOR: Remove this catch. + $errorMessage = $this->handleModelManagerException($e); + + $isFormValid = false; + } catch (ModelManagerThrowable $e) { + $errorMessage = $this->handleModelManagerThrowable($e); + + $isFormValid = false; + } catch (LockException) { + $this->addFlash('sonata_flash_error', $this->trans('flash_lock_error', [ + '%name%' => $this->escapeHtml($this->admin->toString($existingObject)), + '%link_start%' => sprintf('', $this->admin->generateObjectUrl('edit', $existingObject)), + '%link_end%' => '', + ], 'SonataAdminBundle')); + } + } + + // show an error message if the form failed validation + if (!$isFormValid) { + $response = $this->handleXmlHttpRequestErrorResponse($request, $form); + + if ($response !== null && $this->isXmlHttpRequest($request)) { + return $response; + } + + $this->addFlash( + 'sonata_flash_error', + $errorMessage ?? $this->trans( + 'flash_edit_error', + ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))], + 'SonataAdminBundle', + ), + ); + } elseif ($this->isPreviewRequested($request)) { + // enable the preview template if the form was valid and preview was requested + $templateKey = 'preview'; + $this->admin->getShow(); + } + } + + $formView = $form->createView(); + // set the theme for the current Admin Form + $this->setFormTheme($formView, $this->admin->getFormTheme()); + + $template = $this->admin->getTemplateRegistry()->getTemplate($templateKey); + + /** + * @psalm-suppress DeprecatedMethod + */ + return $this->renderWithExtraParams($template, [ + 'action' => 'edit', + 'form' => $formView, + 'object' => $existingObject, + 'objectId' => $objectId, + ]); + } +} diff --git a/packages/administration/src/DependencyInjection/ShopsysAdministrationExtension.php b/packages/administration/src/DependencyInjection/ShopsysAdministrationExtension.php new file mode 100644 index 00000000000..83146a49e44 --- /dev/null +++ b/packages/administration/src/DependencyInjection/ShopsysAdministrationExtension.php @@ -0,0 +1,22 @@ +load('services.yaml'); + } +} diff --git a/packages/administration/src/Form/AdminLoginForm.php b/packages/administration/src/Form/AdminLoginForm.php new file mode 100644 index 00000000000..e1d54367143 --- /dev/null +++ b/packages/administration/src/Form/AdminLoginForm.php @@ -0,0 +1,24 @@ +add('email', EmailType::class) + ->add('password', PasswordType::class); + } +} diff --git a/packages/administration/src/Migrations/.gitkeep b/packages/administration/src/Migrations/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/administration/src/Model/Administrator/AdministratorAdmin.php b/packages/administration/src/Model/Administrator/AdministratorAdmin.php new file mode 100644 index 00000000000..af85d769a1b --- /dev/null +++ b/packages/administration/src/Model/Administrator/AdministratorAdmin.php @@ -0,0 +1,117 @@ +with('Basic Information', ['class' => 'col-md-9']) + ->add('username', TextType::class) + ->add('realName') + ->add('email', EmailType::class) + ->add('password', RepeatedType::class, [ + 'label' => 'Password confirmation', + 'type' => PasswordType::class, + 'required' => $this->isCurrentRoute('create'), + ]) + ->end() + ->with('Security', ['class' => 'col-md-3']) + ->add('roleGroup', ChoiceFieldMaskType::class, [ + 'choices' => $this->administratorRoleGroupFacade->getAll(), + 'required' => false, + 'placeholder' => 'Custom', + 'label' => t('Role Group'), + 'choice_label' => function (AdministratorRoleGroup $administratorRoleGroup) { + return $administratorRoleGroup->getName(); + }, + 'map' => [ + null => ['roles'], + ], + ]) + ->add('roles', ChoiceType::class, [ + 'choices' => Roles::getAvailableAdministratorRolesChoices(), + 'placeholder' => t('-- Select a role --'), + 'label' => t('Role'), + 'required' => false, + 'multiple' => true, + ]) + ->end(); + } + + /** + * @param \Sonata\AdminBundle\Datagrid\DatagridMapper $filter + */ + protected function configureDatagridFilters(DatagridMapper $filter): void + { + $filter->add('email'); + } + + /** + * @param \Sonata\AdminBundle\Datagrid\ListMapper $list + */ + protected function configureListFields(ListMapper $list): void + { + $list->addIdentifier('id'); + $list->add('email'); + $list->add('realName'); + + $list->add(ListMapper::NAME_ACTIONS, null, [ + 'actions' => [ + 'edit' => [], + 'delete' => [], + ], + ]); + } + + /** + * @param \Sonata\AdminBundle\Route\RouteCollectionInterface $collection + */ + protected function configureRoutes(RouteCollectionInterface $collection): void + { + $collection->remove('show'); + } + + /** + * @param \Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQueryInterface $query + * @return \Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQueryInterface + */ + protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface + { + /** @var \Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery $query */ + $query = parent::configureQuery($query); + $query->andWhere('o.id != 1'); + + return $query; + } +} diff --git a/packages/administration/src/Model/Administrator/AdministratorManager.php b/packages/administration/src/Model/Administrator/AdministratorManager.php new file mode 100644 index 00000000000..4d9049886c5 --- /dev/null +++ b/packages/administration/src/Model/Administrator/AdministratorManager.php @@ -0,0 +1,86 @@ +administratorDataFactory->create(); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Administrator\AdministratorData $dataObject + * @return \Shopsys\FrameworkBundle\Model\Administrator\Administrator + */ + public function doCreate(AdminIdentifierInterface $dataObject): Administrator + { + return $this->administratorFacade->create($dataObject); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Administrator\Administrator $object + */ + public function doDelete(object $object): void + { + $this->administratorFacade->delete($object->getId()); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Administrator\AdministratorData $dataObject + * @return \Shopsys\FrameworkBundle\Model\Administrator\Administrator + */ + public function doEdit(AdminIdentifierInterface $dataObject): object + { + return $this->administratorFacade->edit($dataObject->getId(), $dataObject); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Administrator\Administrator $entity + * @return \Shopsys\Administration\Component\Security\AdminIdentifierInterface + */ + public function buildDataObjectForEdit(object $entity): AdminIdentifierInterface + { + return $this->administratorDataFactory->createFromAdministrator($entity); + } +} diff --git a/packages/administration/src/Model/Order/OrderAdmin.php b/packages/administration/src/Model/Order/OrderAdmin.php new file mode 100644 index 00000000000..cb7e0f8753e --- /dev/null +++ b/packages/administration/src/Model/Order/OrderAdmin.php @@ -0,0 +1,224 @@ +remove('create'); + } + + /** + * @param \Sonata\AdminBundle\Form\FormMapper $form + */ + protected function configureFormFields(FormMapper $form): void + { + $form->tab('Basic Information') + ->with('Order Information') + ->add('domainId', DisplayOnlyDomainIconType::class, [ + 'label' => t('Domain'), + 'data' => $this->getSubject()->getDomainId(), + ]) + ->add('number', DisplayOnlyType::class, [ + 'label' => t('Order number'), + 'data' => $this->getSubject()->getNumber(), + ]) + ->add('status', EntityType::class, [ + 'class' => 'Shopsys\FrameworkBundle\Model\Order\Status\OrderStatus', + 'choice_label' => 'name', + ]) + ->add('note', TextareaType::class) + ->end() + ->with('Order Items') + ->add('orderItems', OrderItemsType::class, [ + 'order' => $this->getSubject(), + ]) + ->end() + ->end() + ->tab('Customer') + ->with('Customer Information') + ->add('firstName') + ->add('lastName') + ->add('email') + ->add('telephone') + ->end() + ->with('Company information', ['class' => 'col-md-6']) + ->add('companyName') + ->add('companyNumber') + ->add('companyTaxNumber') + ->end() + ->with('Billing Address', ['class' => 'col-md-6']) + ->add('street') + ->add('city') + ->add('postcode') + ->add('country', EntityType::class, [ + 'class' => 'Shopsys\FrameworkBundle\Model\Country\Country', + 'choice_label' => 'name', + ]) + ->end() + ->with('Delivery Address', ['class' => 'col-md-6']) + ->add('deliveryAddressSameAsBillingAddress', ChoiceFieldMaskType::class, [ + 'choices' => [ + 'Yes' => true, + 'No' => false, + ], + 'required' => true, + 'map' => [ + false => [ + 'deliveryFirstName', + 'deliveryLastName', + 'deliveryCompanyName', + 'deliveryStreet', + 'deliveryCity', + 'deliveryPostcode', + 'deliveryCountry', + ], + ], + ]) + ->add('deliveryFirstName', TextType::class) + ->add('deliveryLastName', TextType::class) + ->add('deliveryCompanyName', TextType::class) + ->add('deliveryStreet', TextType::class) + ->add('deliveryCity', TextType::class) + ->add('deliveryPostcode', TextType::class) + ->add('deliveryCountry', EntityType::class, [ + 'class' => 'Shopsys\FrameworkBundle\Model\Country\Country', + 'choice_label' => 'name', + ]) + ->end() + ->end() + ->tab('Other') + ->with('Payment transactions') + ->add('paymentTransactionRefunds', PaymentTransactionsType::class, [ + 'entry_type' => PaymentTransactionType::class, + 'error_bubbling' => false, + 'allow_add' => false, + 'allow_delete' => false, + 'required' => false, + 'order' => $this->getSubject(), + ]) + ->end() + ->end(); + } + + /** + * @param \Sonata\AdminBundle\Datagrid\DatagridMapper $filter + */ + protected function configureDatagridFilters(DatagridMapper $filter): void + { + $filter->add('domainId', ChoiceFilter::class, [ + 'label' => 'Domain', + 'show_filter' => true, + 'field_type' => ChoiceType::class, + 'field_options' => [ + 'choices' => array_flip($this->domain->getAllDomainsAsChoices()), + ], + ]); + $filter->add('number'); + $filter->add('email'); + $filter->add('totalPriceWithVat', NumberFilter::class); + $filter->add('status', null, [ + 'field_type' => EntityType::class, + 'field_options' => [ + 'class' => 'Shopsys\FrameworkBundle\Model\Order\Status\OrderStatus', + 'choice_label' => 'name', + ], + ]); + } + + /** + * @param \Sonata\AdminBundle\Datagrid\ListMapper $list + */ + protected function configureListFields(ListMapper $list): void + { + $list->addIdentifier('id'); + $list->remove('id'); + $list->add('number', null, [ + 'label' => 'Order number', + ]); + $list->add('createdAt'); + $list->add('customerName'); + $list->add('domainId', 'domain', [ + 'label' => 'Domain', + ]); + $list->add('status.name', 'status', [ + 'label' => 'Status', + ]); + $list->add('totalPriceWithVat', null, [ + 'label' => 'Total price', + 'template' => '@ShopsysAdministration/orderPrice.html.twig', + ]); + + $list->add(ListMapper::NAME_ACTIONS, null, [ + 'actions' => [ + 'edit' => [], + 'delete' => [], + ], + ]); + } + + /** + * @param \Sonata\AdminBundle\Datagrid\ProxyQueryInterface $query + * @return \Sonata\AdminBundle\Datagrid\ProxyQueryInterface + */ + protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface + { + /** @var \Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery $query */ + $query = parent::configureQuery($query); + + if ($this->isCurrentRoute('list')) { + $query->addSelect('(CASE WHEN o.companyName IS NOT NULL + THEN o.companyName + ELSE CONCAT(o.lastName, \' \', o.firstName) + END) AS customerName') + ->andWhere('o.deleted = FALSE'); + } + + return $query; + } + + /** + * @param array $sortValues + */ + protected function configureDefaultSortValues(array &$sortValues): void + { + $sortValues[DatagridInterface::SORT_ORDER] = 'DESC'; + $sortValues[DatagridInterface::SORT_BY] = 'createdAt'; + } +} diff --git a/packages/administration/src/Model/Order/OrderManager.php b/packages/administration/src/Model/Order/OrderManager.php new file mode 100644 index 00000000000..0c220da9c59 --- /dev/null +++ b/packages/administration/src/Model/Order/OrderManager.php @@ -0,0 +1,87 @@ +orderDataFactory->create(); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Order\Order $object + */ + public function doDelete(object $object): void + { + $this->orderFacade->deleteById($object->getId()); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Order\OrderData $dataObject + * @return object + */ + public function doCreate(AdminIdentifierInterface $dataObject): object + { + throw new Exception('Creation of order in administration is not supported.'); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Order\OrderData $dataObject + * @return object + */ + public function doEdit(AdminIdentifierInterface $dataObject): object + { + return $this->orderFacade->edit($dataObject->getId(), $dataObject); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Order\Order $entity + * @return \Shopsys\Administration\Component\Security\AdminIdentifierInterface + */ + public function buildDataObjectForEdit(object $entity): AdminIdentifierInterface + { + return $this->orderDataFactory->createFromOrder($entity); + } +} diff --git a/packages/administration/src/Model/Products/ProductAdmin.php b/packages/administration/src/Model/Products/ProductAdmin.php new file mode 100644 index 00000000000..507c9dcf7e0 --- /dev/null +++ b/packages/administration/src/Model/Products/ProductAdmin.php @@ -0,0 +1,114 @@ +addIdentifier('id'); + $list->add('name'); + $list->add('catnum'); + $list->add('sellingDenied'); + + $list->add(ListMapper::NAME_ACTIONS, null, [ + 'actions' => [ + 'edit' => [], + 'delete' => [], + ], + ]); + } + + /** + * @param \Sonata\AdminBundle\Form\FormMapper $form + */ + protected function configureFormFields(FormMapper $form): void + { + $form->with('Basic information') + ->add('name', LocalizedFullWidthType::class) + ->add('catnum') + ->add('ean') + ->add('sellingDenied', YesNoType::class) + ->end() + ->with('Parameters') + ->add('parameters', CollectionType::class, [ + 'entry_type' => ProductParameterValueFormType::class, + 'constraints' => [ + new UniqueProductParameters([ + 'message' => 'Parameter {{ parameterName }} is used more than once', + ]), + ], + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => false, + ], [ + 'edit' => 'inline', + 'inline' => 'table', + ]) + ->end(); + + $form + ->get('parameters') + ->addModelTransformer($this->productParameterValueToProductParameterValuesLocalizedTransformer); + } + + /** + * @param \Sonata\AdminBundle\Datagrid\DatagridMapper $filter + */ + protected function configureDatagridFilters(DatagridMapper $filter): void + { + $filter->add('catnum'); + $filter->add('sellingDenied'); + } + + /** + * @param \Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery $query + * @return \Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery + */ + protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface + { + /** @var \Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery $query */ + $query = parent::configureQuery($query); + $query + ->addSelect('pmip.inputPrice AS priceForProductList') + ->leftJoin( + ProductManualInputPrice::class, + 'pmip', + Join::WITH, + 'pmip.product = o.id AND pmip.pricingGroup = :pricingGroupId', + ) + ->setParameters([ + 'pricingGroupId' => 1, + ]); + + return $query; + } +} diff --git a/packages/administration/src/Model/Products/ProductManager.php b/packages/administration/src/Model/Products/ProductManager.php new file mode 100644 index 00000000000..824340ab062 --- /dev/null +++ b/packages/administration/src/Model/Products/ProductManager.php @@ -0,0 +1,87 @@ +productDataFactory->create(); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\ProductData $dataObject + * @return \Shopsys\FrameworkBundle\Model\Product\Product + */ + public function doCreate(AdminIdentifierInterface $dataObject): object + { + return $this->productFacade->create($dataObject, ProductRecalculationPriorityEnum::HIGH); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Product $object + */ + public function doDelete(object $object): void + { + $this->productFacade->delete($object->getId()); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\ProductData $dataObject + * @return \Shopsys\FrameworkBundle\Model\Product\Product + */ + public function doEdit(AdminIdentifierInterface $dataObject): object + { + return $this->productFacade->edit($dataObject->getId(), $dataObject, ProductRecalculationPriorityEnum::HIGH); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Product $entity + * @return \Shopsys\Administration\Component\Security\AdminIdentifierInterface + */ + public function buildDataObjectForEdit(object $entity): AdminIdentifierInterface + { + return $this->productDataFactory->createFromProduct($entity); + } +} diff --git a/packages/administration/src/Resources/config/services.yaml b/packages/administration/src/Resources/config/services.yaml new file mode 100644 index 00000000000..af7652c6abf --- /dev/null +++ b/packages/administration/src/Resources/config/services.yaml @@ -0,0 +1,53 @@ +services: + _defaults: + autoconfigure: true + autowire: true + public: false + + Shopsys\Administration\: + resource: '../../**/*{Authenticator,Factory,Manager}.php' + + Shopsys\Administration\Component\FieldDescription\FieldDescriptionFactory: + arguments: + $registry: '@doctrine' + + Shopsys\Administration\Controller\: + public: true + resource: '../../Controller/' + + Shopsys\Administration\Controller\CRUDController: + calls: + - [ setContainer, [ '@service_container' ] ] + public: true + tags: + - 'container.service_subscriber' + + admin.administrator: + calls: + - [ setModelManager, [ '@Shopsys\Administration\Model\Administrator\AdministratorManager' ] ] + class: Shopsys\Administration\Model\Administrator\AdministratorAdmin + tags: + - { name: sonata.admin, model_class: Shopsys\FrameworkBundle\Model\Administrator\Administrator, manager_type: orm, label: Administrator, group: 'Admin', on_top: true } + + admin.order: + calls: + - [ setModelManager, [ '@Shopsys\Administration\Model\Order\OrderManager' ]] + class: Shopsys\Administration\Model\Order\OrderAdmin + tags: + - { name: sonata.admin, model_class: Shopsys\FrameworkBundle\Model\Order\Order, manager_type: orm, label: Order, on_top: true } + + admin.product: + calls: + - [ setModelManager, [ '@Shopsys\Administration\Model\Products\ProductManager' ] ] + class: Shopsys\Administration\Model\Products\ProductAdmin + tags: + - { name: sonata.admin, model_class: Shopsys\FrameworkBundle\Model\Product\Product, manager_type: orm, label: Products, group: 'Products', on_top: true } + + sonata.admin.field_description_factory.orm: + arguments: + $registry: '@doctrine' + class: Shopsys\Administration\Component\FieldDescription\FieldDescriptionFactory + + sonata.admin.request.fetcher: + class: Sonata\AdminBundle\Request\AdminFetcher + public: true diff --git a/packages/administration/src/Resources/views/domain.html.twig b/packages/administration/src/Resources/views/domain.html.twig new file mode 100644 index 00000000000..e3c20d3a8d8 --- /dev/null +++ b/packages/administration/src/Resources/views/domain.html.twig @@ -0,0 +1,5 @@ +{% extends '@SonataAdmin/CRUD/base_list_field.html.twig' %} + +{% block field %} + {{ domainIcon(value, 'small') }} +{% endblock %} \ No newline at end of file diff --git a/packages/administration/src/Resources/views/orderPrice.html.twig b/packages/administration/src/Resources/views/orderPrice.html.twig new file mode 100644 index 00000000000..c826ea1d23f --- /dev/null +++ b/packages/administration/src/Resources/views/orderPrice.html.twig @@ -0,0 +1,5 @@ +{% extends '@SonataAdmin/CRUD/base_list_field.html.twig' %} + +{% block field %} + {{ value|priceWithCurrency(object.currency) }} +{% endblock %} \ No newline at end of file diff --git a/packages/administration/src/Resources/views/security/login.html.twig b/packages/administration/src/Resources/views/security/login.html.twig new file mode 100644 index 00000000000..dff63a0e89f --- /dev/null +++ b/packages/administration/src/Resources/views/security/login.html.twig @@ -0,0 +1,60 @@ +{% extends '@SonataAdmin/standard_layout.html.twig' %} + +{% block sonata_nav %} +{% endblock sonata_nav %} + +{% block logo %} +{% endblock logo %} + +{% block sonata_left_side %} +{% endblock sonata_left_side %} + +{% block body_attributes %}class="sonata-bc login-page"{% endblock %} + +{% block sonata_wrapper %} + +{% endblock sonata_wrapper %} \ No newline at end of file diff --git a/packages/administration/src/ShopsysAdministrationBundle.php b/packages/administration/src/ShopsysAdministrationBundle.php new file mode 100644 index 00000000000..d8236c09052 --- /dev/null +++ b/packages/administration/src/ShopsysAdministrationBundle.php @@ -0,0 +1,19 @@ +getCurrentDomainConfig()->getDateTimeZone(); } + + /** + * @return array + */ + public function getAllDomainsAsChoices(): array + { + $choices = []; + + foreach ($this->getAll() as $domainConfig) { + $choices[$domainConfig->getId()] = $domainConfig->getName(); + } + + return $choices; + } } diff --git a/packages/framework/src/Component/EntityExtension/EntityManagerDecorator.php b/packages/framework/src/Component/EntityExtension/EntityManagerDecorator.php index 49431ec77f8..857c755efad 100644 --- a/packages/framework/src/Component/EntityExtension/EntityManagerDecorator.php +++ b/packages/framework/src/Component/EntityExtension/EntityManagerDecorator.php @@ -77,18 +77,6 @@ public function find($entityName, $id, $lockMode = null, $lockVersion = null): ? return parent::find($resolvedEntityName, $id, $lockMode, $lockVersion); } - /** - * {@inheritdoc} - */ - public function clear($objectName = null): void - { - if ($objectName !== null) { - $objectName = $this->entityNameResolver->resolve($objectName); - } - - parent::clear($objectName); - } - /** * @param string $className */ diff --git a/packages/framework/src/Model/Administrator/Administrator.php b/packages/framework/src/Model/Administrator/Administrator.php index 4b3d334d0c3..2e0796ada7b 100644 --- a/packages/framework/src/Model/Administrator/Administrator.php +++ b/packages/framework/src/Model/Administrator/Administrator.php @@ -469,4 +469,12 @@ public function setTransferIssuesLastSeenDateTime($transferIssuesLastSeenDateTim { $this->transferIssuesLastSeenDateTime = $transferIssuesLastSeenDateTime; } + + /** + * @return string + */ + public function __toString(): string + { + return sprintf(t('Administrator with ID: %s'), $this->id); + } } diff --git a/packages/framework/src/Model/Administrator/AdministratorData.php b/packages/framework/src/Model/Administrator/AdministratorData.php index fd42fd9b8c5..75ff265e027 100644 --- a/packages/framework/src/Model/Administrator/AdministratorData.php +++ b/packages/framework/src/Model/Administrator/AdministratorData.php @@ -5,18 +5,35 @@ namespace Shopsys\FrameworkBundle\Model\Administrator; use DateTime; +use Shopsys\Administration\Component\Security\AdminIdentifierInterface; use Shopsys\FrameworkBundle\Model\Security\Roles; +use Symfony\Component\Validator\Constraints as Assert; -class AdministratorData +class AdministratorData implements AdminIdentifierInterface { + /** + * @var int|null + */ + public $id; + /** * @var string|null */ + #[Assert\NotBlank(message: 'Please enter username')] + #[Assert\Length( + max: 100, + maxMessage: 'Username cannot be longer than {{ limit }} characters', + )] public $username; /** * @var string|null */ + #[Assert\NotBlank(message: 'Please enter full name')] + #[Assert\Length( + max: 100, + maxMessage: 'Full name cannot be longer than {{ limit }} characters', + )] public $realName; /** @@ -27,6 +44,14 @@ class AdministratorData /** * @var string|null */ + #[Assert\Email( + message: 'Please enter valid email', + )] + #[Assert\NotBlank(message: 'Please enter email')] + #[Assert\Length( + max: 255, + maxMessage: 'Email cannot be longer than {{ limit }} characters', + )] public $email; /** @@ -44,4 +69,12 @@ public function __construct() $this->roles[] = Roles::ROLE_ADMIN; $this->transferIssuesLastSeenDateTime = new DateTime('1970-01-01 00:00:00'); } + + /** + * @return int|null + */ + public function getId(): ?int + { + return $this->id; + } } diff --git a/packages/framework/src/Model/Administrator/AdministratorDataFactory.php b/packages/framework/src/Model/Administrator/AdministratorDataFactory.php index 01ea48099a0..ccec05bc295 100644 --- a/packages/framework/src/Model/Administrator/AdministratorDataFactory.php +++ b/packages/framework/src/Model/Administrator/AdministratorDataFactory.php @@ -40,6 +40,7 @@ public function createFromAdministrator(Administrator $administrator): Administr */ protected function fillFromAdministrator(AdministratorData $administratorData, Administrator $administrator) { + $administratorData->id = $administrator->getId(); $administratorData->email = $administrator->getEmail(); $administratorData->realName = $administrator->getRealName(); $administratorData->username = $administrator->getUsername(); diff --git a/packages/framework/src/Model/Order/Order.php b/packages/framework/src/Model/Order/Order.php index cf5acbb51fc..fe8ce794407 100644 --- a/packages/framework/src/Model/Order/Order.php +++ b/packages/framework/src/Model/Order/Order.php @@ -1125,4 +1125,12 @@ public function getTrackingUrl(): ?string OrderMail::TRANSPORT_VARIABLE_TRACKING_NUMBER => $trackingNumber, ]); } + + /** + * @return string + */ + public function __toString(): string + { + return sprintf('Order with id: %s and number: %s', $this->id, $this->number); + } } diff --git a/packages/framework/src/Model/Order/OrderData.php b/packages/framework/src/Model/Order/OrderData.php index 4d8b4155be6..5aa4c949a04 100644 --- a/packages/framework/src/Model/Order/OrderData.php +++ b/packages/framework/src/Model/Order/OrderData.php @@ -4,8 +4,15 @@ namespace Shopsys\FrameworkBundle\Model\Order; -class OrderData +use Shopsys\Administration\Component\Security\AdminIdentifierInterface; + +class OrderData implements AdminIdentifierInterface { + /** + * @var int|null + */ + public $id; + public const NEW_ITEM_PREFIX = 'new_'; /** @@ -232,6 +239,14 @@ public function __construct() $this->isCompanyCustomer = false; } + /** + * @return int|null + */ + public function getId(): ?int + { + return $this->id; + } + /** * @return \Shopsys\FrameworkBundle\Model\Order\Item\OrderItemData[] */ diff --git a/packages/framework/src/Model/Order/OrderDataFactory.php b/packages/framework/src/Model/Order/OrderDataFactory.php index a5bd84d11fd..ffdeac35bda 100644 --- a/packages/framework/src/Model/Order/OrderDataFactory.php +++ b/packages/framework/src/Model/Order/OrderDataFactory.php @@ -53,6 +53,7 @@ public function createFromOrder(Order $order): OrderData */ protected function fillFromOrder(OrderData $orderData, Order $order): void { + $orderData->id = $order->getId(); $orderData->orderNumber = $order->getNumber(); $orderData->status = $order->getStatus(); $orderData->firstName = $order->getFirstName(); diff --git a/packages/framework/src/Model/Product/ProductData.php b/packages/framework/src/Model/Product/ProductData.php index 6af85dc9acc..ab5f86511ed 100644 --- a/packages/framework/src/Model/Product/ProductData.php +++ b/packages/framework/src/Model/Product/ProductData.php @@ -4,10 +4,13 @@ namespace Shopsys\FrameworkBundle\Model\Product; +use Shopsys\Administration\Component\Security\AdminIdentifierInterface; use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\UrlListData; -class ProductData +class ProductData implements AdminIdentifierInterface { + public $id; + /** * @var string[]|null[] */ @@ -223,4 +226,12 @@ public function __construct() $this->saleExclusion = []; $this->domainHidden = []; } + + /** + * @return int|null + */ + public function getId(): ?int + { + return $this->id; + } } diff --git a/packages/framework/src/Resources/translations/messages.cs.po b/packages/framework/src/Resources/translations/messages.cs.po index efeb4c612cb..c1a0196e6ce 100644 --- a/packages/framework/src/Resources/translations/messages.cs.po +++ b/packages/framework/src/Resources/translations/messages.cs.po @@ -154,6 +154,9 @@ msgstr "Administrátor {{ name }} je jediný a nemůže být sm msgid "Administrator activities history" msgstr "Historie aktivit administrátora" +msgid "Administrator with ID: %s" +msgstr "" + msgid "Administrators" msgstr "Administrátoři" diff --git a/packages/framework/src/Resources/translations/messages.en.po b/packages/framework/src/Resources/translations/messages.en.po index f5bb5ad0230..9c507b475e8 100644 --- a/packages/framework/src/Resources/translations/messages.en.po +++ b/packages/framework/src/Resources/translations/messages.en.po @@ -154,6 +154,9 @@ msgstr "" msgid "Administrator activities history" msgstr "" +msgid "Administrator with ID: %s" +msgstr "" + msgid "Administrators" msgstr "" diff --git a/project-base/app/composer.json b/project-base/app/composer.json index 70839da0f99..6a971a86c56 100644 --- a/project-base/app/composer.json +++ b/project-base/app/composer.json @@ -72,6 +72,7 @@ "scheb/2fa-qr-code": "^5.7", "sensio/framework-extra-bundle": "^5.2", "sentry/sentry-symfony": "^4.2.8", + "shopsys/administration": "15.0.x-dev", "shopsys/deployment": "~2.1.0", "shopsys/form-types-bundle": "15.0.x-dev", "shopsys/framework": "15.0.x-dev", diff --git a/project-base/app/config/bundles.php b/project-base/app/config/bundles.php index c9b5f864ed3..ee345d4de8e 100644 --- a/project-base/app/config/bundles.php +++ b/project-base/app/config/bundles.php @@ -43,4 +43,12 @@ Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true], Sentry\SentryBundle\SentryBundle::class => ['prod' => true], Overblog\DataLoaderBundle\OverblogDataLoaderBundle::class => ['all' => true], + Sonata\Twig\Bridge\Symfony\SonataTwigBundle::class => ['all' => true], + Sonata\Doctrine\Bridge\Symfony\SonataDoctrineBundle::class => ['all' => true], + Sonata\Form\Bridge\Symfony\SonataFormBundle::class => ['all' => true], + Sonata\Exporter\Bridge\Symfony\SonataExporterBundle::class => ['all' => true], + Sonata\BlockBundle\SonataBlockBundle::class => ['all' => true], + Sonata\AdminBundle\SonataAdminBundle::class => ['all' => true], + Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle::class => ['all' => true], + Shopsys\Administration\ShopsysAdministrationBundle::class => ['all' => true], ]; diff --git a/project-base/app/config/packages/security.yaml b/project-base/app/config/packages/security.yaml index 626412233fa..9b64cce7fe5 100644 --- a/project-base/app/config/packages/security.yaml +++ b/project-base/app/config/packages/security.yaml @@ -163,6 +163,22 @@ security: id: Shopsys\FrontendApiBundle\Model\User\FrontendApiUserProvider firewalls: + admin-new: + pattern: ^/admin-new/(.*) + user_checker: Shopsys\FrameworkBundle\Model\Security\AdministratorChecker + provider: administrators + entry_point: form_login + form_login: + login_path: sonata_admin_login + use_forward: false + check_path: sonata_admin_login + failure_path: null + logout: + path: sonata_admin_logout + target: sonata_admin_login + guard: + authenticators: + - Shopsys\Administration\Component\Security\AdminLoginAuthenticator # see Shopsys\FrameworkBundle\Model\Administrator\Security\AdministratorFrontSecurityFacade administration: pattern: ^/(%admin_url%/|efconnect|elfinder) @@ -201,6 +217,9 @@ security: - App\FrontendApi\Model\Token\TokenAuthenticator access_control: + - { path: ^/admin-new/login$, role: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/admin-new/logout$, role: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/admin-new/, role: [ ROLE_ADMIN, ROLE_SONATA_ADMIN ] } # This makes the logout route accessible during two-factor authentication. Allows the user to # cancel two-factor authentication, if they need to. - { path: ^/%admin_url%/logout, role: PUBLIC_ACCESS } diff --git a/project-base/app/config/packages/sonata_admin.yaml b/project-base/app/config/packages/sonata_admin.yaml new file mode 100644 index 00000000000..fd25fc411b1 --- /dev/null +++ b/project-base/app/config/packages/sonata_admin.yaml @@ -0,0 +1,26 @@ +sonata_admin: + assets: + extra_javascripts: + - bundles/shopsysadmin/js/admin.js + extra_stylesheets: + - bundles/shopsysadmin/css/admin.css + dashboard: + blocks: + - { type: sonata.admin.block.admin_list, position: left } + default_controller: Shopsys\Administration\Controller\CRUDController + options: + default_group: Orders + show_mosaic_button: false + title: 'Shopsys Platform admin' + title_logo: public/admin/images/logo.svg + +sonata_block: + blocks: + sonata.admin.block.admin_list: + contexts: [admin] + +sonata_doctrine_orm_admin: + templates: + types: + list: + domain: '@ShopsysAdministration/domain.html.twig' diff --git a/project-base/app/config/packages/sonata_block.yaml b/project-base/app/config/packages/sonata_block.yaml new file mode 100644 index 00000000000..b83465ccb54 --- /dev/null +++ b/project-base/app/config/packages/sonata_block.yaml @@ -0,0 +1,2 @@ +sonata_block: + http_cache: false diff --git a/project-base/app/config/packages/sonata_form.yaml b/project-base/app/config/packages/sonata_form.yaml new file mode 100644 index 00000000000..d540e7f4172 --- /dev/null +++ b/project-base/app/config/packages/sonata_form.yaml @@ -0,0 +1,2 @@ +sonata_form: + form_type: standard diff --git a/project-base/app/config/routes/sonata_admin.yaml b/project-base/app/config/routes/sonata_admin.yaml new file mode 100644 index 00000000000..48477200b3c --- /dev/null +++ b/project-base/app/config/routes/sonata_admin.yaml @@ -0,0 +1,12 @@ + +_sonata_admin: + prefix: /admin-new + resource: . + type: sonata_admin +admin_area: + prefix: /admin-new + resource: "@SonataAdminBundle/Resources/config/routing/sonata_admin.xml" + +sonata_admin: + resource: "@ShopsysAdministrationBundle/Controller" + type: annotation diff --git a/project-base/app/docker/php-fpm/Dockerfile b/project-base/app/docker/php-fpm/Dockerfile index 25967c9feee..e8e62098a38 100644 --- a/project-base/app/docker/php-fpm/Dockerfile +++ b/project-base/app/docker/php-fpm/Dockerfile @@ -56,7 +56,7 @@ RUN curl -L \ -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/rate_limit -RUN composer install --optimize-autoloader --no-interaction --no-progress --dev -vvv +RUN composer install --optimize-autoloader --no-interaction --no-progress --dev RUN php phing build-deploy-part-1-db-independent diff --git a/project-base/app/src/Model/Product/ProductDataFactory.php b/project-base/app/src/Model/Product/ProductDataFactory.php index 6de217a2d11..4a2a28e29d9 100644 --- a/project-base/app/src/Model/Product/ProductDataFactory.php +++ b/project-base/app/src/Model/Product/ProductDataFactory.php @@ -185,6 +185,7 @@ protected function fillFromProduct(BaseProductData $productData, BaseProduct $pr $productData->urls->mainFriendlyUrlsByDomainId[$domainId] = $mainFriendlyUrl; } + $productData->id = $product->getId(); $productData->catnum = $product->getCatnum(); $productData->partno = $product->getPartno(); $productData->ean = $product->getEan(); diff --git a/project-base/app/src/Model/Product/Transfer/Akeneo/TransferredProductProcessor.php b/project-base/app/src/Model/Product/Transfer/Akeneo/TransferredProductProcessor.php index 494cf4ae796..593aa40b6b1 100644 --- a/project-base/app/src/Model/Product/Transfer/Akeneo/TransferredProductProcessor.php +++ b/project-base/app/src/Model/Product/Transfer/Akeneo/TransferredProductProcessor.php @@ -235,7 +235,7 @@ private function createProductImage( $this->filesystem->write($tempFileName, $mediaFileResponse->getBody()->getContents()); $createdImage = $this->imageFacade->uploadAndReturnImage($product, [$akeneoMediaFileName], null, false); - $this->em->clear(Image::class); + $this->em->clear(); $image = $this->imageFacade->getById($createdImage->getId()); $image->setAkeneoCode($akeneoMediaFileName); diff --git a/project-base/app/tests/App/Smoke/Http/RouteConfigCustomization.php b/project-base/app/tests/App/Smoke/Http/RouteConfigCustomization.php index 6d5042f43c8..1167e17d666 100644 --- a/project-base/app/tests/App/Smoke/Http/RouteConfigCustomization.php +++ b/project-base/app/tests/App/Smoke/Http/RouteConfigCustomization.php @@ -70,7 +70,7 @@ private function filterRoutesForTesting(RouteConfigCustomizer $routeConfigCustom } }) ->customize(function (RouteConfig $config, RouteInfo $info) { - if (!preg_match('~^(admin|front)_~', $info->getRouteName())) { + if (!preg_match('~^(admin(?!_new)|front)_~', $info->getRouteName())) { $config->skipRoute('Only routes for front-end and administration are tested.'); } }) diff --git a/project-base/app/translations/validators.cs.po b/project-base/app/translations/validators.cs.po index cbc1243a56b..2ebe6b05679 100644 --- a/project-base/app/translations/validators.cs.po +++ b/project-base/app/translations/validators.cs.po @@ -34,6 +34,9 @@ msgstr "Barva příznaku musí mít správný formát hexadecimálního kódu, n msgid "Flag name cannot be longer than {{ limit }} characters" msgstr "Název příznaku nemůže být delší než {{ limit }} znaků" +msgid "Full name cannot be longer than {{ limit }} characters" +msgstr "" + msgid "Identification number cannot be longer than {{ limit }} characters" msgstr "IČ nesmí být delší než {{ limit }} znaků" @@ -103,6 +106,9 @@ msgstr "Vyplňte prosím barvu příznaku" msgid "Please enter flag name in all languages" msgstr "Prosím zadejte název příznaku ve všech jazycích" +msgid "Please enter full name" +msgstr "" + msgid "Please enter identification number" msgstr "Prosím vyplňte IČ" @@ -139,6 +145,9 @@ msgstr "Vyplňte prosím telefonní číslo" msgid "Please enter the message" msgstr "Vyplňte prosím zprávu" +msgid "Please enter username" +msgstr "" + msgid "Please enter valid email" msgstr "Vyplňte prosím platný e-mail" @@ -217,6 +226,9 @@ msgstr "Uploadovaný obrázek je příliš velký ({{ size }} {{ suffix }}). Max msgid "User with provided email address does not exist." msgstr "Uživatel s uvedenou e-mailovou adresou neexistuje." +msgid "Username cannot be longer than {{ limit }} characters" +msgstr "" + msgid "Variable {{ needle }} is required" msgstr "Proměnná {{ needle }} je povinná" diff --git a/project-base/app/translations/validators.en.po b/project-base/app/translations/validators.en.po index e27080a501d..a7738258621 100644 --- a/project-base/app/translations/validators.en.po +++ b/project-base/app/translations/validators.en.po @@ -34,6 +34,9 @@ msgstr "" msgid "Flag name cannot be longer than {{ limit }} characters" msgstr "" +msgid "Full name cannot be longer than {{ limit }} characters" +msgstr "" + msgid "Identification number cannot be longer than {{ limit }} characters" msgstr "" @@ -103,6 +106,9 @@ msgstr "" msgid "Please enter flag name in all languages" msgstr "" +msgid "Please enter full name" +msgstr "" + msgid "Please enter identification number" msgstr "" @@ -139,6 +145,9 @@ msgstr "" msgid "Please enter the message" msgstr "" +msgid "Please enter username" +msgstr "" + msgid "Please enter valid email" msgstr "" @@ -217,6 +226,9 @@ msgstr "" msgid "User with provided email address does not exist." msgstr "" +msgid "Username cannot be longer than {{ limit }} characters" +msgstr "" + msgid "Variable {{ needle }} is required" msgstr "" diff --git a/symfony.lock b/symfony.lock index e3e257c0b27..feebf4f4ba6 100644 --- a/symfony.lock +++ b/symfony.lock @@ -633,6 +633,74 @@ "project-base/config/packages/snc_redis.yaml" ] }, + "sonata-project/admin-bundle": { + "version": "4.22", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "4.0", + "ref": "0e5931df1732e3dccfba42a20853049e5e9db6ae" + }, + "files": [ + "project-base/app/config/packages/sonata_admin.yaml", + "project-base/app/config/routes/sonata_admin.yaml", + "project-base/app/src/Admin/.gitignore" + ] + }, + "sonata-project/block-bundle": { + "version": "4.21", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "4.11", + "ref": "b4edd2a1e6ac1827202f336cac2771cb529de542" + }, + "files": [ + "project-base/app/config/packages/sonata_block.yaml" + ] + }, + "sonata-project/doctrine-extensions": { + "version": "2.1", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.8", + "ref": "4ea4a4b6730f83239608d7d4c849533645c70169" + } + }, + "sonata-project/doctrine-orm-admin-bundle": { + "version": "4.9.1" + }, + "sonata-project/exporter": { + "version": "3.3", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "2.4", + "ref": "93d6df022ef1dc24bdfa8667ddd560bbde89a7cc" + } + }, + "sonata-project/form-extensions": { + "version": "1.18", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.4", + "ref": "9c8a1e8ce2b1f215015ed16652c4ed18eb5867fd" + }, + "files": [ + "project-base/app/config/packages/sonata_form.yaml" + ] + }, + "sonata-project/twig-extensions": { + "version": "2.4", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.2", + "ref": "30dba2f9b719f21b497a6302f41aac07f9079e13" + } + }, "squizlabs/php_codesniffer": { "version": "3.0", "recipe": {