diff --git a/composer.json b/composer.json index 61c8062bdc..6c19064617 100644 --- a/composer.json +++ b/composer.json @@ -34,29 +34,29 @@ "sonata-project/exporter": "^1.11 || ^2.0", "sonata-project/form-extensions": "^0.1.1 || ^1.4", "sonata-project/twig-extensions": "^0.1.1 || ^1.3", - "symfony/asset": "^4.4", - "symfony/config": "^4.4", - "symfony/console": "^4.4", - "symfony/dependency-injection": "^4.4.3", - "symfony/doctrine-bridge": "^4.4", - "symfony/event-dispatcher": "^4.4", + "symfony/asset": "^4.4 || ^5.1", + "symfony/config": "^4.4 || ^5.1", + "symfony/console": "^4.4 || ^5.1", + "symfony/dependency-injection": "^4.4.3 || ^5.1", + "symfony/doctrine-bridge": "^4.4 || ^5.1", + "symfony/event-dispatcher": "^4.4 || ^5.1", "symfony/event-dispatcher-contracts": "^1.1 || ^2.0", "symfony/expression-language": "^4.4 || ^5.1", - "symfony/form": "^4.4.12", - "symfony/framework-bundle": "^4.4", + "symfony/form": "^4.4.12 || ^5.1", + "symfony/framework-bundle": "^4.4 || ^5.1", "symfony/http-foundation": "^4.4 || ^5.1", - "symfony/http-kernel": "^4.4", + "symfony/http-kernel": "^4.4 || ^5.1", "symfony/options-resolver": "^4.4 || ^5.1", "symfony/property-access": "^4.4 || ^5.1", - "symfony/routing": "^4.4", + "symfony/routing": "^4.4 || ^5.1", "symfony/security-acl": "^3.1", - "symfony/security-bundle": "^4.4", - "symfony/security-core": "^4.4", - "symfony/security-csrf": "^4.4", + "symfony/security-bundle": "^4.4 || ^5.1", + "symfony/security-core": "^4.4 || ^5.1", + "symfony/security-csrf": "^4.4 || ^5.1", "symfony/string": "^5.1", - "symfony/translation": "^4.4", - "symfony/twig-bridge": "^4.4", - "symfony/twig-bundle": "^4.4", + "symfony/translation": "^4.4 || ^5.1", + "symfony/twig-bridge": "^4.4 || ^5.1", + "symfony/twig-bundle": "^4.4 || ^5.1", "symfony/validator": "^4.4 || ^5.1", "twig/string-extra": "^3.0", "twig/twig": "^2.12.1 || ^3.0" @@ -75,7 +75,7 @@ "phpstan/phpstan": "^0.12.29", "psalm/plugin-symfony": "^1.4", "psr/event-dispatcher": "^1.0", - "sonata-project/intl-bundle": "^2.4", + "sonata-project/intl-bundle": "^2.9", "symfony/browser-kit": "^4.4 || ^5.1", "symfony/css-selector": "^4.4 || ^5.1", "symfony/filesystem": "^4.4 || ^5.1", diff --git a/src/Admin/AbstractAdmin.php b/src/Admin/AbstractAdmin.php index 98f4c12d88..725031476e 100644 --- a/src/Admin/AbstractAdmin.php +++ b/src/Admin/AbstractAdmin.php @@ -54,9 +54,10 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface as RoutingUrlGeneratorInterface; use Symfony\Component\Security\Acl\Model\DomainObjectInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Translation\TranslatorInterface as DeprecatedTranslatorInterface; use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Thomas Rabaix @@ -311,7 +312,7 @@ abstract class AbstractAdmin implements AdminInterface, DomainObjectInterface, A * * NEXT_MAJOR: remove this property * - * @var \Symfony\Component\Translation\TranslatorInterface + * @var DeprecatedTranslatorInterface|TranslatorInterface * * @deprecated since sonata-project/admin-bundle 3.9, to be removed with 4.0 */ @@ -2475,8 +2476,10 @@ public function getTranslationDomain() * NEXT_MAJOR: remove this method * * @deprecated since sonata-project/admin-bundle 3.9, to be removed with 4.0 + * + * @param DeprecatedTranslatorInterface|TranslatorInterface $translator */ - public function setTranslator(TranslatorInterface $translator) + public function setTranslator($translator) { $args = \func_get_args(); if (isset($args[1]) && $args[1]) { @@ -2486,6 +2489,16 @@ public function setTranslator(TranslatorInterface $translator) ), E_USER_DEPRECATED); } + if (!$translator instanceof DeprecatedTranslatorInterface && !$translator instanceof TranslatorInterface) { + throw new \TypeError(sprintf( + 'Argument 1 passed to "%s()" must be an instance of "%s" or "%s", %s given.', + __METHOD__, + DeprecatedTranslatorInterface::class, + TranslatorInterface::class, + \is_object($translator) ? 'instance of '.\get_class($translator) : \gettype($translator) + )); + } + $this->translator = $translator; } diff --git a/src/Controller/CRUDController.php b/src/Controller/CRUDController.php index 9c0a4b2b71..74d9e2cc1c 100644 --- a/src/Controller/CRUDController.php +++ b/src/Controller/CRUDController.php @@ -25,7 +25,6 @@ use Sonata\AdminBundle\Templating\TemplateRegistryInterface; use Sonata\AdminBundle\Util\AdminObjectAclData; use Sonata\AdminBundle\Util\AdminObjectAclManipulator; -use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\DependencyInjection\ContainerInterface; diff --git a/src/Controller/ControllerTrait.php b/src/Controller/ControllerTrait.php new file mode 100644 index 0000000000..3e402eaffa --- /dev/null +++ b/src/Controller/ControllerTrait.php @@ -0,0 +1,442 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Controller; + +use Doctrine\Persistence\ManagerRegistry; +use Psr\Container\ContainerInterface; +use Psr\Link\LinkInterface; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Stamp\StampInterface; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener; +use Symfony\Component\WebLink\GenericLinkProvider; + +/** + * NEXT_MAJOR: Remove this trait. + * + * @see https://github.com/symfony/symfony/blob/4.4/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php + * + * @internal + * + * @property ContainerInterface $container + */ +trait ControllerTrait +{ + /** + * Returns true if the service id is defined. + * + * @final + */ + protected function has(string $id): bool + { + return $this->container->has($id); + } + + /** + * Gets a container service by its id. + * + * @return object The service + * + * @final + */ + protected function get(string $id) + { + return $this->container->get($id); + } + + /** + * Generates a URL from the given parameters. + * + * @see UrlGeneratorInterface + * + * @final + */ + protected function generateUrl(string $route, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string + { + return $this->container->get('router')->generate($route, $parameters, $referenceType); + } + + /** + * Forwards the request to another controller. + * + * @param string $controller The controller name (a string like Bundle\BlogBundle\Controller\PostController::indexAction) + * + * @final + */ + protected function forward(string $controller, array $path = [], array $query = []): Response + { + $request = $this->container->get('request_stack')->getCurrentRequest(); + $path['_controller'] = $controller; + $subRequest = $request->duplicate($query, null, $path); + + return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST); + } + + /** + * Returns a RedirectResponse to the given URL. + * + * @final + */ + protected function redirect(string $url, int $status = 302): RedirectResponse + { + return new RedirectResponse($url, $status); + } + + /** + * Returns a RedirectResponse to the given route with the given parameters. + * + * @final + */ + protected function redirectToRoute(string $route, array $parameters = [], int $status = 302): RedirectResponse + { + return $this->redirect($this->generateUrl($route, $parameters), $status); + } + + /** + * Returns a JsonResponse that uses the serializer component if enabled, or json_encode. + * + * @final + */ + protected function json($data, int $status = 200, array $headers = [], array $context = []): JsonResponse + { + if ($this->container->has('serializer')) { + $json = $this->container->get('serializer')->serialize($data, 'json', array_merge([ + 'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS, + ], $context)); + + return new JsonResponse($json, $status, $headers, true); + } + + return new JsonResponse($data, $status, $headers); + } + + /** + * Returns a BinaryFileResponse object with original or customized file name and disposition header. + * + * @param \SplFileInfo|string $file File object or path to file to be sent as response + * + * @final + */ + protected function file($file, ?string $fileName = null, string $disposition = ResponseHeaderBag::DISPOSITION_ATTACHMENT): BinaryFileResponse + { + $response = new BinaryFileResponse($file); + $response->setContentDisposition($disposition, null === $fileName ? $response->getFile()->getFilename() : $fileName); + + return $response; + } + + /** + * Adds a flash message to the current session for type. + * + * @throws \LogicException + * + * @final + */ + protected function addFlash(string $type, $message) + { + if (!$this->container->has('session')) { + throw new \LogicException('You can not use the addFlash method if sessions are disabled. Enable them in "config/packages/framework.yaml".'); + } + + $this->container->get('session')->getFlashBag()->add($type, $message); + } + + /** + * Checks if the attributes are granted against the current authentication token and optionally supplied subject. + * + * @throws \LogicException + * + * @final + */ + protected function isGranted($attributes, $subject = null): bool + { + if (!$this->container->has('security.authorization_checker')) { + throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".'); + } + + return $this->container->get('security.authorization_checker')->isGranted($attributes, $subject); + } + + /** + * Throws an exception unless the attributes are granted against the current authentication token and optionally + * supplied subject. + * + * @throws AccessDeniedException + * + * @final + */ + protected function denyAccessUnlessGranted($attributes, $subject = null, string $message = 'Access Denied.') + { + if (!$this->isGranted($attributes, $subject)) { + $exception = $this->createAccessDeniedException($message); + $exception->setAttributes($attributes); + $exception->setSubject($subject); + + throw $exception; + } + } + + /** + * Returns a rendered view. + * + * @final + */ + protected function renderView(string $view, array $parameters = []): string + { + if ($this->container->has('templating')) { + @trigger_error('Using the "templating" service is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); + + return $this->container->get('templating')->render($view, $parameters); + } + + if (!$this->container->has('twig')) { + throw new \LogicException('You can not use the "renderView" method if the Templating Component or the Twig Bundle are not available. Try running "composer require symfony/twig-bundle".'); + } + + return $this->container->get('twig')->render($view, $parameters); + } + + /** + * Renders a view. + * + * @final + */ + protected function render(string $view, array $parameters = [], ?Response $response = null): Response + { + if ($this->container->has('templating')) { + @trigger_error('Using the "templating" service is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); + + $content = $this->container->get('templating')->render($view, $parameters); + } elseif ($this->container->has('twig')) { + $content = $this->container->get('twig')->render($view, $parameters); + } else { + throw new \LogicException('You can not use the "render" method if the Templating Component or the Twig Bundle are not available. Try running "composer require symfony/twig-bundle".'); + } + + if (null === $response) { + $response = new Response(); + } + + $response->setContent($content); + + return $response; + } + + /** + * Streams a view. + * + * @final + */ + protected function stream(string $view, array $parameters = [], ?StreamedResponse $response = null): StreamedResponse + { + if ($this->container->has('templating')) { + @trigger_error('Using the "templating" service is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); + + $templating = $this->container->get('templating'); + + $callback = static function () use ($templating, $view, $parameters) { + $templating->stream($view, $parameters); + }; + } elseif ($this->container->has('twig')) { + $twig = $this->container->get('twig'); + + $callback = static function () use ($twig, $view, $parameters) { + $twig->display($view, $parameters); + }; + } else { + throw new \LogicException('You can not use the "stream" method if the Templating Component or the Twig Bundle are not available. Try running "composer require symfony/twig-bundle".'); + } + + if (null === $response) { + return new StreamedResponse($callback); + } + + $response->setCallback($callback); + + return $response; + } + + /** + * Returns a NotFoundHttpException. + * + * This will result in a 404 response code. Usage example: + * + * throw $this->createNotFoundException('Page not found!'); + * + * @final + */ + protected function createNotFoundException(string $message = 'Not Found', ?\Throwable $previous = null): NotFoundHttpException + { + return new NotFoundHttpException($message, $previous); + } + + /** + * Returns an AccessDeniedException. + * + * This will result in a 403 response code. Usage example: + * + * throw $this->createAccessDeniedException('Unable to access this page!'); + * + * @throws \LogicException If the Security component is not available + * + * @final + */ + protected function createAccessDeniedException(string $message = 'Access Denied.', ?\Throwable $previous = null): AccessDeniedException + { + if (!class_exists(AccessDeniedException::class)) { + throw new \LogicException('You can not use the "createAccessDeniedException" method if the Security component is not available. Try running "composer require symfony/security-bundle".'); + } + + return new AccessDeniedException($message, $previous); + } + + /** + * Creates and returns a Form instance from the type of the form. + * + * @final + */ + protected function createForm(string $type, $data = null, array $options = []): FormInterface + { + return $this->container->get('form.factory')->create($type, $data, $options); + } + + /** + * Creates and returns a form builder instance. + * + * @final + */ + protected function createFormBuilder($data = null, array $options = []): FormBuilderInterface + { + return $this->container->get('form.factory')->createBuilder(FormType::class, $data, $options); + } + + /** + * Shortcut to return the Doctrine Registry service. + * + * @throws \LogicException If DoctrineBundle is not available + * + * @return ManagerRegistry + * + * @final + */ + protected function getDoctrine() + { + if (!$this->container->has('doctrine')) { + throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".'); + } + + return $this->container->get('doctrine'); + } + + /** + * Get a user from the Security Token Storage. + * + * @throws \LogicException If SecurityBundle is not available + * + * @return UserInterface|object|null + * + * @see TokenInterface::getUser() + * + * @final + */ + protected function getUser() + { + if (!$this->container->has('security.token_storage')) { + throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".'); + } + + if (null === $token = $this->container->get('security.token_storage')->getToken()) { + return null; + } + + if (!\is_object($user = $token->getUser())) { + // e.g. anonymous authentication + return null; + } + + return $user; + } + + /** + * Checks the validity of a CSRF token. + * + * @param string $id The id used when generating the token + * @param string|null $token The actual token sent with the request that should be validated + * + * @final + */ + protected function isCsrfTokenValid(string $id, ?string $token): bool + { + if (!$this->container->has('security.csrf.token_manager')) { + throw new \LogicException('CSRF protection is not enabled in your application. Enable it with the "csrf_protection" key in "config/packages/framework.yaml".'); + } + + return $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($id, $token)); + } + + /** + * Dispatches a message to the bus. + * + * @param object|Envelope $message The message or the message pre-wrapped in an envelope + * @param StampInterface[] $stamps + * + * @final + */ + protected function dispatchMessage($message, array $stamps = []): Envelope + { + if (!$this->container->has('messenger.default_bus')) { + $message = class_exists(Envelope::class) ? 'You need to define the "messenger.default_bus" configuration option.' : 'Try running "composer require symfony/messenger".'; + throw new \LogicException('The message bus is not enabled in your application. '.$message); + } + + return $this->container->get('messenger.default_bus')->dispatch($message, $stamps); + } + + /** + * Adds a Link HTTP header to the current response. + * + * @see https://tools.ietf.org/html/rfc5988 + * + * @final + */ + protected function addLink(Request $request, LinkInterface $link) + { + if (!class_exists(AddLinkHeaderListener::class)) { + throw new \LogicException('You can not use the "addLink" method if the WebLink component is not available. Try running "composer require symfony/web-link".'); + } + + if (null === $linkProvider = $request->attributes->get('_links')) { + $request->attributes->set('_links', new GenericLinkProvider([$link])); + + return; + } + + $request->attributes->set('_links', $linkProvider->withLink($link)); + } +} diff --git a/src/Controller/CoreController.php b/src/Controller/CoreController.php index b0e30cc9fe..e0a72f19b7 100644 --- a/src/Controller/CoreController.php +++ b/src/Controller/CoreController.php @@ -26,7 +26,8 @@ use Sonata\AdminBundle\Admin\Pool; use Sonata\AdminBundle\Search\SearchHandler; use Sonata\AdminBundle\Templating\TemplateRegistryInterface; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -34,8 +35,11 @@ /** * @author Thomas Rabaix */ -class CoreController extends Controller +class CoreController implements ContainerAwareInterface { + use ControllerTrait; + use ContainerAwareTrait; + /** * @return Response */ diff --git a/src/Controller/PolyfillControllerTrait.php b/src/Controller/PolyfillControllerTrait.php index 58ba26cdf8..e747d2d4dc 100644 --- a/src/Controller/PolyfillControllerTrait.php +++ b/src/Controller/PolyfillControllerTrait.php @@ -65,6 +65,6 @@ public function proxyCall($method, $arguments) } } -if (!trait_exists(ControllerTrait::class)) { +if (!trait_exists(ControllerTrait::class) && class_exists(Controller::class)) { class_alias(PolyfillControllerTrait::class, ControllerTrait::class); } diff --git a/src/Form/Extension/BaseChoiceTypeExtension.php b/src/Form/Extension/BaseChoiceTypeExtension.php new file mode 100644 index 0000000000..a3ded31141 --- /dev/null +++ b/src/Form/Extension/BaseChoiceTypeExtension.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Form\Extension; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * NEXT_MAJOR: Copy its methods to ChoiceTypeExtension and remove it. + * + * @internal + */ +abstract class BaseChoiceTypeExtension extends AbstractTypeExtension +{ + public function configureOptions(OptionsResolver $resolver) + { + $optionalOptions = ['sortable']; + + $resolver->setDefined($optionalOptions); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['sortable'] = \array_key_exists('sortable', $options) && $options['sortable']; + } + + public function getExtendedType() + { + return ChoiceType::class; + } +} diff --git a/src/Form/Extension/ChoiceTypeExtension.php b/src/Form/Extension/ChoiceTypeExtension.php index 9be85544ec..e8cea9b85c 100644 --- a/src/Form/Extension/ChoiceTypeExtension.php +++ b/src/Form/Extension/ChoiceTypeExtension.php @@ -13,38 +13,36 @@ namespace Sonata\AdminBundle\Form\Extension; -use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\FormInterface; -use Symfony\Component\Form\FormView; -use Symfony\Component\OptionsResolver\OptionsResolver; -/** - * @final since sonata-project/admin-bundle 3.52 - * - * @author Amine Zaghdoudi - */ -class ChoiceTypeExtension extends AbstractTypeExtension -{ - public function configureOptions(OptionsResolver $resolver) - { - $optionalOptions = ['sortable']; - - $resolver->setDefined($optionalOptions); - } - - public function buildView(FormView $view, FormInterface $form, array $options) +use Symfony\Component\Form\FormTypeExtensionInterface; + +// NEXT_MAJOR: Remove the "else" part, copy all methods from BaseChoiceTypeExtension in this class and +// extend from AbstractTypeExtension. +if (method_exists(FormTypeExtensionInterface::class, 'getExtendedTypes')) { + /** + * @final since sonata-project/admin-bundle 3.52 + * + * @author Amine Zaghdoudi + */ + class ChoiceTypeExtension extends BaseChoiceTypeExtension { - $view->vars['sortable'] = \array_key_exists('sortable', $options) && $options['sortable']; + public static function getExtendedTypes(): iterable + { + return [ChoiceType::class]; + } } - - public function getExtendedType() - { - return ChoiceType::class; - } - - public static function getExtendedTypes() +} else { + /** + * @final since sonata-project/admin-bundle 3.52 + * + * @author Amine Zaghdoudi + */ + class ChoiceTypeExtension extends BaseChoiceTypeExtension { - return [ChoiceType::class]; + public static function getExtendedTypes() + { + return [ChoiceType::class]; + } } } diff --git a/src/Form/Extension/Field/Type/BaseFormTypeFieldExtension.php b/src/Form/Extension/Field/Type/BaseFormTypeFieldExtension.php new file mode 100644 index 0000000000..94529d8316 --- /dev/null +++ b/src/Form/Extension/Field/Type/BaseFormTypeFieldExtension.php @@ -0,0 +1,239 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Form\Extension\Field\Type; + +use Sonata\AdminBundle\Admin\FieldDescriptionInterface; +use Sonata\AdminBundle\Exception\NoValueException; +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * NEXT_MAJOR: Copy its methods to FormTypeFieldExtension and remove it. + * + * @internal + */ +abstract class BaseFormTypeFieldExtension extends AbstractTypeExtension +{ + /** + * @var array + */ + protected $defaultClasses = []; + + /** + * @var array + */ + protected $options = []; + + public function __construct(array $defaultClasses, array $options) + { + $this->defaultClasses = $defaultClasses; + $this->options = $options; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $sonataAdmin = [ + 'name' => null, + 'admin' => null, + 'value' => null, + 'edit' => 'standard', + 'inline' => 'natural', + 'field_description' => null, + 'block_name' => false, + 'options' => $this->options, + ]; + + $builder->setAttribute('sonata_admin_enabled', false); + // NEXT_MAJOR: Remove this line + $builder->setAttribute('sonata_help', false); + + if ($options['sonata_field_description'] instanceof FieldDescriptionInterface) { + $fieldDescription = $options['sonata_field_description']; + + $sonataAdmin['admin'] = $fieldDescription->getAdmin(); + $sonataAdmin['field_description'] = $fieldDescription; + $sonataAdmin['name'] = $fieldDescription->getName(); + $sonataAdmin['edit'] = $fieldDescription->getOption('edit', 'standard'); + $sonataAdmin['inline'] = $fieldDescription->getOption('inline', 'natural'); + $sonataAdmin['block_name'] = $fieldDescription->getOption('block_name', false); + $sonataAdmin['class'] = $this->getClass($builder); + + $builder->setAttribute('sonata_admin_enabled', true); + } + + $builder->setAttribute('sonata_admin', $sonataAdmin); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + $sonataAdmin = $form->getConfig()->getAttribute('sonata_admin'); + // NEXT_MAJOR: Remove this line + $sonataAdminHelp = $options['sonata_help'] ?? null; + + /* + * We have a child, so we need to upgrade block prefix + */ + if ($view->parent && $view->parent->vars['sonata_admin_enabled'] && !$sonataAdmin['admin']) { + $blockPrefixes = $view->vars['block_prefixes']; + $baseName = str_replace('.', '_', $view->parent->vars['sonata_admin_code']); + + $baseType = $blockPrefixes[\count($blockPrefixes) - 2]; + $blockSuffix = preg_replace('#^_([a-z0-9]{14})_(.++)$#', '$2', end($blockPrefixes)); + + $blockPrefixes[] = sprintf('%s_%s', $baseName, $baseType); + $blockPrefixes[] = sprintf('%s_%s_%s_%s', $baseName, $baseType, $view->parent->vars['name'], $view->vars['name']); + $blockPrefixes[] = sprintf('%s_%s_%s_%s', $baseName, $baseType, $view->parent->vars['name'], $blockSuffix); + + $view->vars['block_prefixes'] = array_unique($blockPrefixes); + $view->vars['sonata_admin_enabled'] = true; + $view->vars['sonata_admin'] = [ + 'admin' => false, + 'field_description' => false, + 'name' => false, + 'edit' => 'standard', + 'inline' => 'natural', + 'block_name' => false, + 'class' => false, + 'options' => $this->options, + ]; + // NEXT_MAJOR: Remove this line + $view->vars['sonata_help'] = $sonataAdminHelp; + $view->vars['sonata_admin_code'] = $view->parent->vars['sonata_admin_code']; + + return; + } + + // avoid to add extra information not required by non admin field + if ($sonataAdmin && $form->getConfig()->getAttribute('sonata_admin_enabled', true)) { + $sonataAdmin['value'] = $form->getData(); + + // add a new block types, so the Admin Form element can be tweaked based on the admin code + $blockPrefixes = $view->vars['block_prefixes']; + $baseName = str_replace('.', '_', $sonataAdmin['admin']->getCode()); + $baseType = $blockPrefixes[\count($blockPrefixes) - 2]; + $blockSuffix = preg_replace('#^_([a-z0-9]{14})_(.++)$#', '$2', end($blockPrefixes)); + + $blockPrefixes[] = sprintf('%s_%s', $baseName, $baseType); + $blockPrefixes[] = sprintf('%s_%s_%s', $baseName, $sonataAdmin['name'], $baseType); + $blockPrefixes[] = sprintf('%s_%s_%s_%s', $baseName, $sonataAdmin['name'], $baseType, $blockSuffix); + + if (isset($sonataAdmin['block_name']) && false !== $sonataAdmin['block_name']) { + $blockPrefixes[] = $sonataAdmin['block_name']; + } + + $view->vars['block_prefixes'] = array_unique($blockPrefixes); + $view->vars['sonata_admin_enabled'] = true; + $view->vars['sonata_admin'] = $sonataAdmin; + $view->vars['sonata_admin_code'] = $sonataAdmin['admin']->getCode(); + + $attr = $view->vars['attr']; + + if (!isset($attr['class']) && isset($sonataAdmin['class'])) { + $attr['class'] = $sonataAdmin['class']; + } + + $view->vars['attr'] = $attr; + } else { + $view->vars['sonata_admin_enabled'] = false; + } + + // NEXT_MAJOR: Remove this line + $view->vars['sonata_help'] = $sonataAdminHelp; + $view->vars['sonata_admin'] = $sonataAdmin; + } + + public function getExtendedType() + { + return FormType::class; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefaults([ + 'sonata_admin' => null, + 'sonata_field_description' => null, + + // be compatible with mopa if not installed, avoid generating an exception for invalid option + 'label_render' => true, + // NEXT_MAJOR: Remove this property and the deprecation message + 'sonata_help' => null, + ]) + ->setDeprecated( + 'sonata_help', + 'The "sonata_help" option is deprecated since sonata-project/admin-bundle 3.60, to be removed in 4.0. Use "help" instead.' + ); + } + + /** + * return the value related to FieldDescription, if the associated object does no + * exists => a temporary one is created. + * + * @param object|null $object + * + * @return mixed + */ + public function getValueFromFieldDescription($object, FieldDescriptionInterface $fieldDescription) + { + $value = null; + + if (!$object) { + return null; + } + + try { + $value = $fieldDescription->getValue($object); + } catch (NoValueException $e) { + if ($fieldDescription->hasAssociationAdmin()) { + $value = $fieldDescription->getAssociationAdmin()->getNewInstance(); + } + } + + return $value; + } + + /** + * @return string + */ + protected function getClass(FormBuilderInterface $formBuilder) + { + foreach ($this->getTypes($formBuilder) as $type) { + $name = \get_class($type); + + if (isset($this->defaultClasses[$name])) { + return $this->defaultClasses[$name]; + } + } + + return ''; + } + + /** + * @return array + */ + protected function getTypes(FormBuilderInterface $formBuilder) + { + $types = []; + + for ($type = $formBuilder->getType(); null !== $type; $type = $type->getParent()) { + array_unshift($types, $type->getInnerType()); + } + + return $types; + } +} diff --git a/src/Form/Extension/Field/Type/BaseMopaCompatibilityTypeFieldExtension.php b/src/Form/Extension/Field/Type/BaseMopaCompatibilityTypeFieldExtension.php new file mode 100644 index 0000000000..519d731bce --- /dev/null +++ b/src/Form/Extension/Field/Type/BaseMopaCompatibilityTypeFieldExtension.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Form\Extension\Field\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * NEXT_MAJOR: Copy its methods to ChoiceTypeExtension and remove it. + * + * @internal + */ +abstract class BaseMopaCompatibilityTypeFieldExtension extends AbstractTypeExtension +{ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'horizontal_label_class' => '', + 'horizontal_label_offset_class' => '', + 'horizontal_input_wrapper_class' => '', + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['horizontal_label_class'] = $options['horizontal_label_class']; + $view->vars['horizontal_label_offset_class'] = $options['horizontal_label_offset_class']; + $view->vars['horizontal_input_wrapper_class'] = $options['horizontal_input_wrapper_class']; + } + + public function getExtendedType() + { + return FormType::class; + } +} diff --git a/src/Form/Extension/Field/Type/FormTypeFieldExtension.php b/src/Form/Extension/Field/Type/FormTypeFieldExtension.php index 3f97acc649..d29cdef826 100644 --- a/src/Form/Extension/Field/Type/FormTypeFieldExtension.php +++ b/src/Form/Extension/Field/Type/FormTypeFieldExtension.php @@ -13,246 +13,35 @@ namespace Sonata\AdminBundle\Form\Extension\Field\Type; -use Sonata\AdminBundle\Admin\FieldDescriptionInterface; -use Sonata\AdminBundle\Exception\NoValueException; -use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Extension\Core\Type\FormType; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Form\FormInterface; -use Symfony\Component\Form\FormView; -use Symfony\Component\OptionsResolver\OptionsResolver; - -/** - * @final since sonata-project/admin-bundle 3.52 - * - * @author Thomas Rabaix - */ -class FormTypeFieldExtension extends AbstractTypeExtension -{ - /** - * @var array - */ - protected $defaultClasses = []; - - /** - * @var array - */ - protected $options = []; - - public function __construct(array $defaultClasses, array $options) - { - $this->defaultClasses = $defaultClasses; - $this->options = $options; - } - - public function buildForm(FormBuilderInterface $builder, array $options) - { - $sonataAdmin = [ - 'name' => null, - 'admin' => null, - 'value' => null, - 'edit' => 'standard', - 'inline' => 'natural', - 'field_description' => null, - 'block_name' => false, - 'options' => $this->options, - ]; - - $builder->setAttribute('sonata_admin_enabled', false); - // NEXT_MAJOR: Remove this line - $builder->setAttribute('sonata_help', false); - - if ($options['sonata_field_description'] instanceof FieldDescriptionInterface) { - $fieldDescription = $options['sonata_field_description']; - - $sonataAdmin['admin'] = $fieldDescription->getAdmin(); - $sonataAdmin['field_description'] = $fieldDescription; - $sonataAdmin['name'] = $fieldDescription->getName(); - $sonataAdmin['edit'] = $fieldDescription->getOption('edit', 'standard'); - $sonataAdmin['inline'] = $fieldDescription->getOption('inline', 'natural'); - $sonataAdmin['block_name'] = $fieldDescription->getOption('block_name', false); - $sonataAdmin['class'] = $this->getClass($builder); - - $builder->setAttribute('sonata_admin_enabled', true); - } - - $builder->setAttribute('sonata_admin', $sonataAdmin); - } - - public function buildView(FormView $view, FormInterface $form, array $options) - { - $sonataAdmin = $form->getConfig()->getAttribute('sonata_admin'); - // NEXT_MAJOR: Remove this line - $sonataAdminHelp = $options['sonata_help'] ?? null; - - /* - * We have a child, so we need to upgrade block prefix - */ - if ($view->parent && $view->parent->vars['sonata_admin_enabled'] && !$sonataAdmin['admin']) { - $blockPrefixes = $view->vars['block_prefixes']; - $baseName = str_replace('.', '_', $view->parent->vars['sonata_admin_code']); - - $baseType = $blockPrefixes[\count($blockPrefixes) - 2]; - $blockSuffix = preg_replace('#^_([a-z0-9]{14})_(.++)$#', '$2', end($blockPrefixes)); - - $blockPrefixes[] = sprintf('%s_%s', $baseName, $baseType); - $blockPrefixes[] = sprintf('%s_%s_%s_%s', $baseName, $baseType, $view->parent->vars['name'], $view->vars['name']); - $blockPrefixes[] = sprintf('%s_%s_%s_%s', $baseName, $baseType, $view->parent->vars['name'], $blockSuffix); - - $view->vars['block_prefixes'] = array_unique($blockPrefixes); - $view->vars['sonata_admin_enabled'] = true; - $view->vars['sonata_admin'] = [ - 'admin' => false, - 'field_description' => false, - 'name' => false, - 'edit' => 'standard', - 'inline' => 'natural', - 'block_name' => false, - 'class' => false, - 'options' => $this->options, - ]; - // NEXT_MAJOR: Remove this line - $view->vars['sonata_help'] = $sonataAdminHelp; - $view->vars['sonata_admin_code'] = $view->parent->vars['sonata_admin_code']; - - return; - } - - // avoid to add extra information not required by non admin field - if ($sonataAdmin && $form->getConfig()->getAttribute('sonata_admin_enabled', true)) { - $sonataAdmin['value'] = $form->getData(); - - // add a new block types, so the Admin Form element can be tweaked based on the admin code - $blockPrefixes = $view->vars['block_prefixes']; - $baseName = str_replace('.', '_', $sonataAdmin['admin']->getCode()); - $baseType = $blockPrefixes[\count($blockPrefixes) - 2]; - $blockSuffix = preg_replace('#^_([a-z0-9]{14})_(.++)$#', '$2', end($blockPrefixes)); - - $blockPrefixes[] = sprintf('%s_%s', $baseName, $baseType); - $blockPrefixes[] = sprintf('%s_%s_%s', $baseName, $sonataAdmin['name'], $baseType); - $blockPrefixes[] = sprintf('%s_%s_%s_%s', $baseName, $sonataAdmin['name'], $baseType, $blockSuffix); - - if (isset($sonataAdmin['block_name']) && false !== $sonataAdmin['block_name']) { - $blockPrefixes[] = $sonataAdmin['block_name']; - } - - $view->vars['block_prefixes'] = array_unique($blockPrefixes); - $view->vars['sonata_admin_enabled'] = true; - $view->vars['sonata_admin'] = $sonataAdmin; - $view->vars['sonata_admin_code'] = $sonataAdmin['admin']->getCode(); - - $attr = $view->vars['attr']; - - if (!isset($attr['class']) && isset($sonataAdmin['class'])) { - $attr['class'] = $sonataAdmin['class']; - } - - $view->vars['attr'] = $attr; - } else { - $view->vars['sonata_admin_enabled'] = false; - } - - // NEXT_MAJOR: Remove this line - $view->vars['sonata_help'] = $sonataAdminHelp; - $view->vars['sonata_admin'] = $sonataAdmin; - } - - public function getExtendedType() - { - return FormType::class; - } - - public static function getExtendedTypes() - { - return [FormType::class]; - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver - ->setDefaults([ - 'sonata_admin' => null, - 'sonata_field_description' => null, - - // be compatible with mopa if not installed, avoid generating an exception for invalid option - 'label_render' => true, - // NEXT_MAJOR: Remove this property and the deprecation message - 'sonata_help' => null, - ]); - - // BC layer for symfony/options-resolver < 5.1. - // @todo: Remove the check and the contents of the `else` condition when dropping the support for lower versions. - if (method_exists($resolver, 'getInfo')) { - $resolver - ->setDeprecated( - 'sonata_help', - 'sonata-project/admin-bundle', - '3.60', - 'The %name% option is deprecated since sonata-project/admin-bundle 3.60, to be removed in 4.0. Use "help" instead.' - ); - } else { - $resolver - ->setDeprecated( - 'sonata_help', - 'The "sonata_help" option is deprecated since sonata-project/admin-bundle 3.60, to be removed in 4.0. Use "help" instead.' - ); - } - } +use Symfony\Component\Form\FormTypeExtensionInterface; +// NEXT_MAJOR: Remove the "else" part, copy all methods from BaseFormTypeFieldExtension in this class and +// extend from AbstractTypeExtension. +if (method_exists(FormTypeExtensionInterface::class, 'getExtendedTypes')) { /** - * return the value related to FieldDescription, if the associated object does no - * exists => a temporary one is created. + * @final since sonata-project/admin-bundle 3.52 * - * @param object|null $object - * - * @return mixed + * @author Thomas Rabaix */ - public function getValueFromFieldDescription($object, FieldDescriptionInterface $fieldDescription) + class FormTypeFieldExtension extends BaseFormTypeFieldExtension { - $value = null; - - if (!$object) { - return null; + public static function getExtendedTypes(): iterable + { + return [FormType::class]; } - - try { - $value = $fieldDescription->getValue($object); - } catch (NoValueException $e) { - if ($fieldDescription->hasAssociationAdmin()) { - $value = $fieldDescription->getAssociationAdmin()->getNewInstance(); - } - } - - return $value; } - +} else { /** - * @return string - */ - protected function getClass(FormBuilderInterface $formBuilder) - { - foreach ($this->getTypes($formBuilder) as $type) { - $name = \get_class($type); - - if (isset($this->defaultClasses[$name])) { - return $this->defaultClasses[$name]; - } - } - - return ''; - } - - /** - * @return array + * @final since sonata-project/admin-bundle 3.52 + * + * @author Thomas Rabaix */ - protected function getTypes(FormBuilderInterface $formBuilder) + class FormTypeFieldExtension extends BaseFormTypeFieldExtension { - $types = []; - - for ($type = $formBuilder->getType(); null !== $type; $type = $type->getParent()) { - array_unshift($types, $type->getInnerType()); + public static function getExtendedTypes() + { + return [FormType::class]; } - - return $types; } } diff --git a/src/Form/Extension/Field/Type/MopaCompatibilityTypeFieldExtension.php b/src/Form/Extension/Field/Type/MopaCompatibilityTypeFieldExtension.php index 313ef79a57..e7f24e43fb 100644 --- a/src/Form/Extension/Field/Type/MopaCompatibilityTypeFieldExtension.php +++ b/src/Form/Extension/Field/Type/MopaCompatibilityTypeFieldExtension.php @@ -13,45 +13,41 @@ namespace Sonata\AdminBundle\Form\Extension\Field\Type; -use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Extension\Core\Type\FormType; -use Symfony\Component\Form\FormInterface; -use Symfony\Component\Form\FormView; -use Symfony\Component\OptionsResolver\OptionsResolver; - -/** - * This class is built to allow AdminInterface to work properly - * if the MopaBootstrapBundle is not installed. - * - * @final since sonata-project/admin-bundle 3.52 - * - * @author Thomas Rabaix - */ -class MopaCompatibilityTypeFieldExtension extends AbstractTypeExtension -{ - public function configureOptions(OptionsResolver $resolver) +use Symfony\Component\Form\FormTypeExtensionInterface; + +// NEXT_MAJOR: Remove the "else" part, copy all methods from BaseMopaCompatibilityTypeFieldExtension in this class and +// extend from AbstractTypeExtension +if (method_exists(FormTypeExtensionInterface::class, 'getExtendedTypes')) { + /** + * This class is built to allow AdminInterface to work properly + * if the MopaBootstrapBundle is not installed. + * + * @final since sonata-project/admin-bundle 3.52 + * + * @author Thomas Rabaix + */ + class MopaCompatibilityTypeFieldExtension extends BaseMopaCompatibilityTypeFieldExtension { - $resolver->setDefaults([ - 'horizontal_label_class' => '', - 'horizontal_label_offset_class' => '', - 'horizontal_input_wrapper_class' => '', - ]); + public static function getExtendedTypes(): iterable + { + return [FormType::class]; + } } - - public function buildView(FormView $view, FormInterface $form, array $options) - { - $view->vars['horizontal_label_class'] = $options['horizontal_label_class']; - $view->vars['horizontal_label_offset_class'] = $options['horizontal_label_offset_class']; - $view->vars['horizontal_input_wrapper_class'] = $options['horizontal_input_wrapper_class']; - } - - public function getExtendedType() - { - return FormType::class; - } - - public static function getExtendedTypes() +} else { + /** + * This class is built to allow AdminInterface to work properly + * if the MopaBootstrapBundle is not installed. + * + * @final since sonata-project/admin-bundle 3.52 + * + * @author Thomas Rabaix + */ + class MopaCompatibilityTypeFieldExtension extends BaseMopaCompatibilityTypeFieldExtension { - return [FormType::class]; + public static function getExtendedTypes() + { + return [FormType::class]; + } } } diff --git a/src/Form/Type/Filter/ChoiceType.php b/src/Form/Type/Filter/ChoiceType.php index 91638fb931..db81714397 100644 --- a/src/Form/Type/Filter/ChoiceType.php +++ b/src/Form/Type/Filter/ChoiceType.php @@ -18,7 +18,8 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType as FormChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Translation\TranslatorInterface as DeprecatedTranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @final since sonata-project/admin-bundle 3.52 @@ -47,12 +48,22 @@ class ChoiceType extends AbstractType * * @deprecated since sonata-project/admin-bundle 3.5, to be removed with 4.0 * - * @var TranslatorInterface + * @var DeprecatedTranslatorInterface|TranslatorInterface */ protected $translator; - public function __construct(TranslatorInterface $translator) + public function __construct($translator) { + if (!$translator instanceof DeprecatedTranslatorInterface && !$translator instanceof TranslatorInterface) { + throw new \TypeError(sprintf( + 'Argument 1 passed to "%s()" must be an instance of "%s" or "%s", %s given.', + __METHOD__, + DeprecatedTranslatorInterface::class, + TranslatorInterface::class, + \is_object($translator) ? 'instance of '.\get_class($translator) : \gettype($translator) + )); + } + $this->translator = $translator; } diff --git a/src/Form/Type/Filter/DateRangeType.php b/src/Form/Type/Filter/DateRangeType.php index 01825fc1c0..1b8e678474 100644 --- a/src/Form/Type/Filter/DateRangeType.php +++ b/src/Form/Type/Filter/DateRangeType.php @@ -18,7 +18,8 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Translation\TranslatorInterface as DeprecatedTranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @final since sonata-project/admin-bundle 3.52 @@ -42,12 +43,22 @@ class DateRangeType extends AbstractType * * @deprecated since sonata-project/admin-bundle 3.5, to be removed with 4.0 * - * @var TranslatorInterface + * @var DeprecatedTranslatorInterface|TranslatorInterface */ protected $translator; - public function __construct(TranslatorInterface $translator) + public function __construct($translator) { + if (!$translator instanceof DeprecatedTranslatorInterface && !$translator instanceof TranslatorInterface) { + throw new \TypeError(sprintf( + 'Argument 1 passed to "%s()" must be an instance of "%s" or "%s", %s given.', + __METHOD__, + DeprecatedTranslatorInterface::class, + TranslatorInterface::class, + \is_object($translator) ? 'instance of '.\get_class($translator) : \gettype($translator) + )); + } + $this->translator = $translator; } diff --git a/src/Form/Type/Filter/DateTimeRangeType.php b/src/Form/Type/Filter/DateTimeRangeType.php index 66f5309c56..980264b99b 100644 --- a/src/Form/Type/Filter/DateTimeRangeType.php +++ b/src/Form/Type/Filter/DateTimeRangeType.php @@ -18,7 +18,8 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Translation\TranslatorInterface as DeprecatedTranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @final since sonata-project/admin-bundle 3.52 @@ -42,12 +43,22 @@ class DateTimeRangeType extends AbstractType * * @deprecated since sonata-project/admin-bundle 3.5, to be removed with 4.0 * - * @var TranslatorInterface + * @var DeprecatedTranslatorInterface|TranslatorInterface */ protected $translator; - public function __construct(TranslatorInterface $translator) + public function __construct($translator) { + if (!$translator instanceof DeprecatedTranslatorInterface && !$translator instanceof TranslatorInterface) { + throw new \TypeError(sprintf( + 'Argument 1 passed to "%s()" must be an instance of "%s" or "%s", %s given.', + __METHOD__, + DeprecatedTranslatorInterface::class, + TranslatorInterface::class, + \is_object($translator) ? 'instance of '.\get_class($translator) : \gettype($translator) + )); + } + $this->translator = $translator; } diff --git a/src/Form/Type/Filter/DateTimeType.php b/src/Form/Type/Filter/DateTimeType.php index 4bb209c850..41f9977025 100644 --- a/src/Form/Type/Filter/DateTimeType.php +++ b/src/Form/Type/Filter/DateTimeType.php @@ -18,7 +18,8 @@ use Symfony\Component\Form\Extension\Core\Type\DateTimeType as FormDateTimeType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Translation\TranslatorInterface as DeprecatedTranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @final since sonata-project/admin-bundle 3.52 @@ -67,12 +68,22 @@ class DateTimeType extends AbstractType * * @deprecated since sonata-project/admin-bundle 3.5, to be removed with 4.0 * - * @var TranslatorInterface + * @var DeprecatedTranslatorInterface|TranslatorInterface */ protected $translator; - public function __construct(TranslatorInterface $translator) + public function __construct($translator) { + if (!$translator instanceof DeprecatedTranslatorInterface && !$translator instanceof TranslatorInterface) { + throw new \TypeError(sprintf( + 'Argument 1 passed to "%s()" must be an instance of "%s" or "%s", %s given.', + __METHOD__, + DeprecatedTranslatorInterface::class, + TranslatorInterface::class, + \is_object($translator) ? 'instance of '.\get_class($translator) : \gettype($translator) + )); + } + $this->translator = $translator; } diff --git a/src/Form/Type/Filter/DateType.php b/src/Form/Type/Filter/DateType.php index 24cefd185e..f9ce1f5dff 100644 --- a/src/Form/Type/Filter/DateType.php +++ b/src/Form/Type/Filter/DateType.php @@ -18,7 +18,8 @@ use Symfony\Component\Form\Extension\Core\Type\DateType as FormDateType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Translation\TranslatorInterface as DeprecatedTranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @final since sonata-project/admin-bundle 3.52 @@ -67,12 +68,22 @@ class DateType extends AbstractType * * @deprecated since sonata-project/admin-bundle 3.5, to be removed with 4.0 * - * @var TranslatorInterface + * @var DeprecatedTranslatorInterface|TranslatorInterface */ protected $translator; - public function __construct(TranslatorInterface $translator) + public function __construct($translator) { + if (!$translator instanceof DeprecatedTranslatorInterface && !$translator instanceof TranslatorInterface) { + throw new \TypeError(sprintf( + 'Argument 1 passed to "%s()" must be an instance of "%s" or "%s", %s given.', + __METHOD__, + DeprecatedTranslatorInterface::class, + TranslatorInterface::class, + \is_object($translator) ? 'instance of '.\get_class($translator) : \gettype($translator) + )); + } + $this->translator = $translator; } diff --git a/src/Form/Type/Filter/NumberType.php b/src/Form/Type/Filter/NumberType.php index 4da94e8a31..b9baa83af6 100644 --- a/src/Form/Type/Filter/NumberType.php +++ b/src/Form/Type/Filter/NumberType.php @@ -18,7 +18,8 @@ use Symfony\Component\Form\Extension\Core\Type\NumberType as FormNumberType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Translation\TranslatorInterface as DeprecatedTranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @final since sonata-project/admin-bundle 3.52 @@ -57,12 +58,22 @@ class NumberType extends AbstractType * * @deprecated since sonata-project/admin-bundle 3.5, to be removed with 4.0 * - * @var TranslatorInterface + * @var DeprecatedTranslatorInterface|TranslatorInterface */ protected $translator; - public function __construct(TranslatorInterface $translator) + public function __construct($translator) { + if (!$translator instanceof DeprecatedTranslatorInterface && !$translator instanceof TranslatorInterface) { + throw new \TypeError(sprintf( + 'Argument 1 passed to "%s()" must be an instance of "%s" or "%s", %s given.', + __METHOD__, + DeprecatedTranslatorInterface::class, + TranslatorInterface::class, + \is_object($translator) ? 'instance of '.\get_class($translator) : \gettype($translator) + )); + } + $this->translator = $translator; } diff --git a/tests/Admin/AdminTest.php b/tests/Admin/AdminTest.php index 4f16463c76..3936318f68 100644 --- a/tests/Admin/AdminTest.php +++ b/tests/Admin/AdminTest.php @@ -83,10 +83,11 @@ use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Translation\TranslatorInterface as DeprecatedTranslatorInterface; use Symfony\Component\Validator\Mapping\MemberMetadata; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class AdminTest extends TestCase { @@ -1155,7 +1156,7 @@ public function testGetTranslator(): void $this->assertNull($admin->getTranslator()); - $translator = $this->createMock(TranslatorInterface::class); + $translator = $this->createStub($this->getTranslatorInterface()); $admin->setTranslator($translator); $this->assertSame($translator, $admin->getTranslator()); @@ -1430,7 +1431,7 @@ public function testTrans(): void $admin = new PostAdmin('sonata.post.admin.post', 'NewsBundle\Entity\Post', 'Sonata\NewsBundle\Controller\PostAdminController'); $admin->setTranslationDomain('fooMessageDomain'); - $translator = $this->createMock(TranslatorInterface::class); + $translator = $this->createStub($this->getTranslatorInterface()); $admin->setTranslator($translator); $translator->expects($this->once()) @@ -1448,7 +1449,7 @@ public function testTransWithMessageDomain(): void { $admin = new PostAdmin('sonata.post.admin.post', 'NewsBundle\Entity\Post', 'Sonata\NewsBundle\Controller\PostAdminController'); - $translator = $this->createMock(TranslatorInterface::class); + $translator = $this->createStub($this->getTranslatorInterface()); $admin->setTranslator($translator); $translator->expects($this->once()) @@ -1464,10 +1465,14 @@ public function testTransWithMessageDomain(): void */ public function testTransChoice(): void { + if (!interface_exists(DeprecatedTranslatorInterface::class)) { + $this->markTestSkipped('Test only available in Symfony 4'); + } + $admin = new PostAdmin('sonata.post.admin.post', 'NewsBundle\Entity\Post', 'Sonata\NewsBundle\Controller\PostAdminController'); $admin->setTranslationDomain('fooMessageDomain'); - $translator = $this->createMock(TranslatorInterface::class); + $translator = $this->createMock(DeprecatedTranslatorInterface::class); $admin->setTranslator($translator); $translator->expects($this->once()) @@ -1483,9 +1488,13 @@ public function testTransChoice(): void */ public function testTransChoiceWithMessageDomain(): void { + if (!interface_exists(DeprecatedTranslatorInterface::class)) { + $this->markTestSkipped('Test only available in Symfony 4'); + } + $admin = new PostAdmin('sonata.post.admin.post', 'NewsBundle\Entity\Post', 'Sonata\NewsBundle\Controller\PostAdminController'); - $translator = $this->createMock(TranslatorInterface::class); + $translator = $this->createStub($this->getTranslatorInterface()); $admin->setTranslator($translator); $translator->expects($this->once()) @@ -2773,4 +2782,13 @@ public function getDeprecatedAbstractAdminConstructorArgs(): iterable [1, 1, 1], ]; } + + private function getTranslatorInterface(): string + { + if (interface_exists(DeprecatedTranslatorInterface::class)) { + return DeprecatedTranslatorInterface::class; + } + + return TranslatorInterface::class; + } } diff --git a/tests/Command/GenerateObjectAclCommandTest.php b/tests/Command/GenerateObjectAclCommandTest.php index 55ba4dd544..40dc639d6a 100644 --- a/tests/Command/GenerateObjectAclCommandTest.php +++ b/tests/Command/GenerateObjectAclCommandTest.php @@ -20,7 +20,6 @@ use Sonata\AdminBundle\Admin\Pool; use Sonata\AdminBundle\Command\GenerateObjectAclCommand; use Sonata\AdminBundle\Util\ObjectAclManipulatorInterface; -use Symfony\Bridge\Doctrine\RegistryInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Tester\CommandTester; @@ -66,7 +65,7 @@ public function testExecuteWithDeprecatedDoctrineService(): void { $pool = new Pool($this->container, '', ''); - $registry = $this->prophesize(RegistryInterface::class)->reveal(); + $registry = $this->prophesize(ManagerRegistry::class)->reveal(); $command = new GenerateObjectAclCommand($pool, [], $registry); $application = new Application(); diff --git a/tests/Controller/CRUDControllerTest.php b/tests/Controller/CRUDControllerTest.php index 70198cf25e..d73ccaf3fe 100644 --- a/tests/Controller/CRUDControllerTest.php +++ b/tests/Controller/CRUDControllerTest.php @@ -42,7 +42,6 @@ use Sonata\Exporter\Writer\JsonWriter; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\FrameworkBundle\Controller\Controller; -use Symfony\Bundle\FrameworkBundle\Templating\DelegatingEngine; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Form\Form; @@ -176,8 +175,6 @@ protected function setUp(): void $this->templateRegistry = $this->prophesize(TemplateRegistryInterface::class); - $templating = $this->createMock(DelegatingEngine::class); - $templatingRenderReturnCallback = $this->returnCallback(function ( $view, array $parameters = [], @@ -185,24 +182,18 @@ protected function setUp(): void ) { $this->template = $view; - if (null === $response) { - $response = new Response(); - } - $this->parameters = $parameters; - return $response; + return ''; }); - $templating - ->method('render') - ->will($templatingRenderReturnCallback); - $this->session = new Session(new MockArraySessionStorage()); - $twig = $this->getMockBuilder(Environment::class) - ->disableOriginalConstructor() - ->getMock(); + $twig = $this->createMock(Environment::class); + + $twig + ->method('render') + ->will($templatingRenderReturnCallback); $twig ->method('getRuntime') @@ -253,7 +244,6 @@ protected function setUp(): void $this->container->set('request_stack', $requestStack); $this->container->set('foo.admin', $this->admin); $this->container->set('foo.admin.template_registry', $this->templateRegistry->reveal()); - $this->container->set('templating', $templating); $this->container->set('twig', $twig); $this->container->set('session', $this->session); $this->container->set('sonata.admin.exporter', $exporter); @@ -979,6 +969,7 @@ public function testDeleteActionChildDeprecation(): void $admin->expects($this->once()) ->method('getObject') ->willReturn($object2); + $admin->method('getIdParameter')->willReturn('id'); $this->admin->expects($this->once()) ->method('getObject') diff --git a/tests/Form/Type/Filter/DateTimeRangeTypeTest.php b/tests/Form/Type/Filter/DateTimeRangeTypeTest.php index 6741e8cced..94afe0825d 100644 --- a/tests/Form/Type/Filter/DateTimeRangeTypeTest.php +++ b/tests/Form/Type/Filter/DateTimeRangeTypeTest.php @@ -17,13 +17,13 @@ use Sonata\Form\Type\DateTimeRangeType as FormDateTimeRangeType; use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class DateTimeRangeTypeTest extends TypeTestCase { public function testGetDefaultOptions(): void { - $stub = $this->getMockForAbstractClass(TranslatorInterface::class); + $stub = $this->createStub(TranslatorInterface::class); $type = new DateTimeRangeType($stub); diff --git a/tests/Form/Widget/FormSonataFilterChoiceWidgetTest.php b/tests/Form/Widget/FormSonataFilterChoiceWidgetTest.php index b2ab5e061a..91cfe89ba8 100644 --- a/tests/Form/Widget/FormSonataFilterChoiceWidgetTest.php +++ b/tests/Form/Widget/FormSonataFilterChoiceWidgetTest.php @@ -18,7 +18,7 @@ use Sonata\AdminBundle\Tests\Fixtures\TestExtension; use Symfony\Component\Form\Extension\Core\Type\ChoiceType as SymfonyChoiceType; use Symfony\Component\Form\FormTypeGuesserInterface; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class FormSonataFilterChoiceWidgetTest extends BaseWidgetTest { @@ -63,7 +63,7 @@ protected function getChoiceClass() protected function getExtensions() { - $mock = $this->getMockBuilder(TranslatorInterface::class)->getMock(); + $mock = $this->createMock(TranslatorInterface::class); $mock->method('trans') ->willReturnCallback( diff --git a/tests/Translator/Extractor/JMSTranslatorBundle/AdminExtractorTest.php b/tests/Translator/Extractor/JMSTranslatorBundle/AdminExtractorTest.php index 7e56bbaa14..09b60a069f 100644 --- a/tests/Translator/Extractor/JMSTranslatorBundle/AdminExtractorTest.php +++ b/tests/Translator/Extractor/JMSTranslatorBundle/AdminExtractorTest.php @@ -23,6 +23,7 @@ use Sonata\AdminBundle\Admin\Pool; use Sonata\AdminBundle\Translator\Extractor\JMSTranslatorBundle\AdminExtractor; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Translation\TranslatorInterface; /** * Test for AdminExtractor. @@ -62,6 +63,10 @@ class AdminExtractorTest extends TestCase protected function setUp(): void { + if (!interface_exists(TranslatorInterface::class)) { + $this->markTestSkipped('This test is only available using Symfony 4'); + } + if (!interface_exists(ExtractorInterface::class)) { $this->markTestSkipped('JMS Translator Bundle does not exist'); } diff --git a/tests/Twig/Extension/SonataAdminExtensionTest.php b/tests/Twig/Extension/SonataAdminExtensionTest.php index 54dad24d8f..c461230f8f 100644 --- a/tests/Twig/Extension/SonataAdminExtensionTest.php +++ b/tests/Twig/Extension/SonataAdminExtensionTest.php @@ -290,6 +290,10 @@ public function testConstructThrowsExceptionWithWrongTranslationArgument(): void */ public function testConstructWithLegacyTranslator(): void { + if (!interface_exists(LegacyTranslatorInterface::class)) { + $this->markTestSkipped('This test is only valid for Symfony 4'); + } + new SonataAdminExtension( $this->pool, null,