From 7963e9d7d04b166704a853ea6c9f0efd885d06bc Mon Sep 17 00:00:00 2001 From: Jules Pietri Date: Tue, 18 Jul 2023 12:31:36 +0200 Subject: [PATCH] [FrameworkBundle] Add parameters deprecations to the output of `debug:container` command --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../Command/ContainerDebugCommand.php | 8 ++++- .../Console/Descriptor/Descriptor.php | 9 ++++-- .../Console/Descriptor/JsonDescriptor.php | 26 ++++++++++++++-- .../Console/Descriptor/MarkdownDescriptor.php | 17 ++++++++-- .../Console/Descriptor/TextDescriptor.php | 31 ++++++++++++++----- .../Console/Descriptor/XmlDescriptor.php | 16 ++++++++-- .../Descriptor/AbstractDescriptorTestCase.php | 11 ++++++- .../Console/Descriptor/ObjectsProvider.php | 11 +++++++ .../Descriptor/deprecated_parameter.json | 4 +++ .../Descriptor/deprecated_parameter.md | 6 ++++ .../Descriptor/deprecated_parameter.txt | 6 ++++ .../Descriptor/deprecated_parameter.xml | 2 ++ .../Descriptor/deprecated_parameters.json | 7 +++++ .../Descriptor/deprecated_parameters.md | 5 +++ .../Descriptor/deprecated_parameters.txt | 11 +++++++ .../Descriptor/deprecated_parameters.xml | 5 +++ 17 files changed, 157 insertions(+), 19 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.json create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.md create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.txt create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.json create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.md create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.txt create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.xml diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 5204de4980c48..3ba4fa7165926 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -30,6 +30,7 @@ CHANGELOG * Change BrowserKitAssertionsTrait::getClient() to be protected * Deprecate the `framework.asset_mapper.provider` config option * Add `--exclude` option to the `cache:pool:clear` command + * Add parameters deprecations to the output of `debug:container` command 6.3 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php index cd1af0d5d43c0..df6aef5dd6b3e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php @@ -129,10 +129,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $options['filter'] = $this->filterToServiceTypes(...); } elseif ($input->getOption('parameters')) { $parameters = []; - foreach ($object->getParameterBag()->all() as $k => $v) { + $parameterBag = $object->getParameterBag(); + foreach ($parameterBag->all() as $k => $v) { $parameters[$k] = $object->resolveEnvPlaceholders($v); } $object = new ParameterBag($parameters); + if ($parameterBag instanceof ParameterBag) { + foreach ($parameterBag->allDeprecated() as $k => $deprecation) { + $object->deprecate($k, ...$deprecation); + } + } $options = []; } elseif ($parameter = $input->getOption('parameter')) { $options = ['parameter' => $parameter]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php index 54a66a1b93e12..ba500adb2bbca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php @@ -43,6 +43,11 @@ public function describe(OutputInterface $output, mixed $object, array $options (new AnalyzeServiceReferencesPass(false, false))->process($object); } + $deprecatedParameters = []; + if ($object instanceof ContainerBuilder && isset($options['parameter']) && ($parameterBag = $object->getParameterBag()) instanceof ParameterBag) { + $deprecatedParameters = $parameterBag->allDeprecated(); + } + match (true) { $object instanceof RouteCollection => $this->describeRouteCollection($object, $options), $object instanceof Route => $this->describeRoute($object, $options), @@ -50,7 +55,7 @@ public function describe(OutputInterface $output, mixed $object, array $options $object instanceof ContainerBuilder && !empty($options['env-vars']) => $this->describeContainerEnvVars($this->getContainerEnvVars($object), $options), $object instanceof ContainerBuilder && isset($options['group_by']) && 'tags' === $options['group_by'] => $this->describeContainerTags($object, $options), $object instanceof ContainerBuilder && isset($options['id']) => $this->describeContainerService($this->resolveServiceDefinition($object, $options['id']), $options, $object), - $object instanceof ContainerBuilder && isset($options['parameter']) => $this->describeContainerParameter($object->resolveEnvPlaceholders($object->getParameter($options['parameter'])), $options), + $object instanceof ContainerBuilder && isset($options['parameter']) => $this->describeContainerParameter($object->resolveEnvPlaceholders($object->getParameter($options['parameter'])), $deprecatedParameters[$options['parameter']] ?? null, $options), $object instanceof ContainerBuilder && isset($options['deprecations']) => $this->describeContainerDeprecations($object, $options), $object instanceof ContainerBuilder => $this->describeContainerServices($object, $options), $object instanceof Definition => $this->describeContainerDefinition($object, $options), @@ -107,7 +112,7 @@ abstract protected function describeContainerDefinition(Definition $definition, abstract protected function describeContainerAlias(Alias $alias, array $options = [], ContainerBuilder $container = null): void; - abstract protected function describeContainerParameter(mixed $parameter, array $options = []): void; + abstract protected function describeContainerParameter(mixed $parameter, ?array $deprecation, array $options = []): void; abstract protected function describeContainerEnvVars(array $envs, array $options = []): void; diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php index 8b109de5219e5..1dc567a3fd345 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php @@ -150,11 +150,16 @@ protected function describeCallable(mixed $callable, array $options = []): void $this->writeData($this->getCallableData($callable), $options); } - protected function describeContainerParameter(mixed $parameter, array $options = []): void + protected function describeContainerParameter(mixed $parameter, ?array $deprecation, array $options = []): void { $key = $options['parameter'] ?? ''; + $data = [$key => $parameter]; - $this->writeData([$key => $parameter], $options); + if ($deprecation) { + $data['_deprecation'] = sprintf('Since %s %s: %s', $deprecation[0], $deprecation[1], sprintf(...\array_slice($deprecation, 2))); + } + + $this->writeData($data, $options); } protected function describeContainerEnvVars(array $envs, array $options = []): void @@ -223,6 +228,23 @@ protected function getRouteData(Route $route): array return $data; } + protected function sortParameters(ParameterBag $parameters): array + { + $sortedParameters = parent::sortParameters($parameters); + + if ($deprecated = $parameters->allDeprecated()) { + $deprecations = []; + + foreach ($deprecated as $parameter => $deprecation) { + $deprecations[$parameter] = sprintf('Since %s %s: %s', $deprecation[0], $deprecation[1], sprintf(...\array_slice($deprecation, 2))); + } + + $sortedParameters['_deprecations'] = $deprecations; + } + + return $sortedParameters; + } + private function getContainerDefinitionData(Definition $definition, bool $omitTags = false, bool $showArguments = false, ContainerBuilder $container = null, string $id = null): array { $data = [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php index 2a5f62cfdcb54..275294d7e2ac6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php @@ -71,9 +71,16 @@ protected function describeRoute(Route $route, array $options = []): void protected function describeContainerParameters(ParameterBag $parameters, array $options = []): void { + $deprecatedParameters = $parameters->allDeprecated(); + $this->write("Container parameters\n====================\n"); foreach ($this->sortParameters($parameters) as $key => $value) { - $this->write(sprintf("\n- `%s`: `%s`", $key, $this->formatParameter($value))); + $this->write(sprintf( + "\n- `%s`: `%s`%s", + $key, + $this->formatParameter($value), + isset($deprecatedParameters[$key]) ? sprintf(' *Since %s %s: %s*', $deprecatedParameters[$key][0], $deprecatedParameters[$key][1], sprintf(...\array_slice($deprecatedParameters[$key], 2))) : '' + )); } } @@ -290,9 +297,13 @@ protected function describeContainerAlias(Alias $alias, array $options = [], Con $this->describeContainerDefinition($container->getDefinition((string) $alias), array_merge($options, ['id' => (string) $alias]), $container); } - protected function describeContainerParameter(mixed $parameter, array $options = []): void + protected function describeContainerParameter(mixed $parameter, ?array $deprecation, array $options = []): void { - $this->write(isset($options['parameter']) ? sprintf("%s\n%s\n\n%s", $options['parameter'], str_repeat('=', \strlen($options['parameter'])), $this->formatParameter($parameter)) : $parameter); + if (isset($options['parameter'])) { + $this->write(sprintf("%s\n%s\n\n%s%s", $options['parameter'], str_repeat('=', \strlen($options['parameter'])), $this->formatParameter($parameter), $deprecation ? sprintf("\n\n*Since %s %s: %s*", $deprecation[0], $deprecation[1], sprintf(...\array_slice($deprecation, 2))) : '')); + } else { + $this->write($parameter); + } } protected function describeContainerEnvVars(array $envs, array $options = []): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php index c1b82ac826366..8a4f812deeb04 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Dumper; use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Argument\AbstractArgument; @@ -124,9 +125,18 @@ protected function describeContainerParameters(ParameterBag $parameters, array $ { $tableHeaders = ['Parameter', 'Value']; + $deprecatedParameters = $parameters->allDeprecated(); + $tableRows = []; foreach ($this->sortParameters($parameters) as $parameter => $value) { $tableRows[] = [$parameter, $this->formatParameter($value)]; + + if (isset($deprecatedParameters[$parameter])) { + $tableRows[] = [new TableCell( + sprintf('(Since %s %s: %s)', $deprecatedParameters[$parameter][0], $deprecatedParameters[$parameter][1], sprintf(...\array_slice($deprecatedParameters[$parameter], 2))), + ['colspan' => 2] + )]; + } } $options['output']->title('Symfony Container Parameters'); @@ -425,14 +435,21 @@ protected function describeContainerAlias(Alias $alias, array $options = [], Con $this->describeContainerDefinition($container->getDefinition((string) $alias), array_merge($options, ['id' => (string) $alias]), $container); } - protected function describeContainerParameter(mixed $parameter, array $options = []): void + protected function describeContainerParameter(mixed $parameter, ?array $deprecation, array $options = []): void { - $options['output']->table( - ['Parameter', 'Value'], - [ - [$options['parameter'], $this->formatParameter($parameter), - ], - ]); + $parameterName = $options['parameter']; + $rows = [ + [$parameterName, $this->formatParameter($parameter)], + ]; + + if ($deprecation) { + $rows[] = [new TableCell( + sprintf('(Since %s %s: %s)', $deprecation[0], $deprecation[1], sprintf(...\array_slice($deprecation, 2))), + ['colspan' => 2] + )]; + } + + $options['output']->table(['Parameter', 'Value'], $rows); } protected function describeContainerEnvVars(array $envs, array $options = []): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php index a6f9ec47d3153..f12e4583b2e53 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php @@ -98,9 +98,9 @@ protected function describeCallable(mixed $callable, array $options = []): void $this->writeDocument($this->getCallableDocument($callable)); } - protected function describeContainerParameter(mixed $parameter, array $options = []): void + protected function describeContainerParameter(mixed $parameter, ?array $deprecation, array $options = []): void { - $this->writeDocument($this->getContainerParameterDocument($parameter, $options)); + $this->writeDocument($this->getContainerParameterDocument($parameter, $deprecation, $options)); } protected function describeContainerEnvVars(array $envs, array $options = []): void @@ -235,10 +235,16 @@ private function getContainerParametersDocument(ParameterBag $parameters): \DOMD $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($parametersXML = $dom->createElement('parameters')); + $deprecatedParameters = $parameters->allDeprecated(); + foreach ($this->sortParameters($parameters) as $key => $value) { $parametersXML->appendChild($parameterXML = $dom->createElement('parameter')); $parameterXML->setAttribute('key', $key); $parameterXML->appendChild(new \DOMText($this->formatParameter($value))); + + if (isset($deprecatedParameters[$key])) { + $parameterXML->setAttribute('deprecated', sprintf('Since %s %s: %s', $deprecatedParameters[$key][0], $deprecatedParameters[$key][1], sprintf(...\array_slice($deprecatedParameters[$key], 2)))); + } } return $dom; @@ -475,13 +481,17 @@ private function getContainerAliasDocument(Alias $alias, string $id = null): \DO return $dom; } - private function getContainerParameterDocument(mixed $parameter, array $options = []): \DOMDocument + private function getContainerParameterDocument(mixed $parameter, ?array $deprecation, array $options = []): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($parameterXML = $dom->createElement('parameter')); if (isset($options['parameter'])) { $parameterXML->setAttribute('key', $options['parameter']); + + if ($deprecation) { + $parameterXML->setAttribute('deprecated', sprintf('Since %s %s: %s', $deprecation[0], $deprecation[1], sprintf(...\array_slice($deprecation, 2)))); + } } $parameterXML->appendChild(new \DOMText($this->formatParameter($parameter))); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTestCase.php index 33c0a55b8f5f8..cc6b08fd236a3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTestCase.php @@ -169,7 +169,13 @@ public static function getDescribeContainerDefinitionWhichIsAnAliasTestData(): a return $data; } - /** @dataProvider getDescribeContainerParameterTestData */ + /** + * The legacy group must be kept as deprecations will always be raised. + * + * @group legacy + * + * @dataProvider getDescribeContainerParameterTestData + */ public function testDescribeContainerParameter($parameter, $expectedDescription, array $options) { $this->assertDescription($expectedDescription, $parameter, $options); @@ -185,6 +191,9 @@ public static function getDescribeContainerParameterTestData(): array $file = array_pop($data[1]); $data[1][] = ['parameter' => 'twig.form.resources']; $data[1][] = $file; + $file = array_pop($data[2]); + $data[2][] = ['parameter' => 'deprecated_foo']; + $data[2][] = $file; return $data; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php index cc9cfad683a72..84adc4ac9bc45 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php @@ -80,6 +80,14 @@ public static function getContainerParameters() 'single' => FooUnitEnum::BAR, ], ]); + + $parameterBag = new ParameterBag([ + 'integer' => 12, + 'string' => 'Hello world!', + ]); + $parameterBag->deprecate('string', 'symfony/framework-bundle', '6.4'); + + yield 'deprecated_parameters' => $parameterBag; } public static function getContainerParameter() @@ -92,10 +100,13 @@ public static function getContainerParameter() 'form_div_layout.html.twig', 'form_table_layout.html.twig', ]); + $builder->setParameter('deprecated_foo', 'bar'); + $builder->deprecateParameter('deprecated_foo', 'symfony/framework-bundle', '6.4'); return [ 'parameter' => $builder, 'array_parameter' => $builder, + 'deprecated_parameter' => $builder, ]; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.json new file mode 100644 index 0000000000000..7ef2d5fb2123d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.json @@ -0,0 +1,4 @@ +{ + "deprecated_foo": "bar", + "_deprecation": "Since symfony\/framework-bundle 6.4: The parameter \"deprecated_foo\" is deprecated." +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.md new file mode 100644 index 0000000000000..f33f58c72fd49 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.md @@ -0,0 +1,6 @@ +deprecated_foo +============== + +bar + +*Since symfony/framework-bundle 6.4: The parameter "deprecated_foo" is deprecated.* diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.txt new file mode 100644 index 0000000000000..29819fe7aea47 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.txt @@ -0,0 +1,6 @@ +-------------------------------------------- ------------------------------------------- +  Parameter   Value  + -------------------------------------------- ------------------------------------------- + deprecated_foo bar + (Since symfony/framework-bundle 6.4: The parameter "deprecated_foo" is deprecated.) + -------------------------------------------- ------------------------------------------- \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.xml new file mode 100644 index 0000000000000..bc8297fe5ed1e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameter.xml @@ -0,0 +1,2 @@ + +bar diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.json new file mode 100644 index 0000000000000..d3d16b4873e6c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.json @@ -0,0 +1,7 @@ +{ + "integer": 12, + "string": "Hello world!", + "_deprecations": { + "string": "Since symfony\/framework-bundle 6.4: The parameter \"string\" is deprecated." + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.md new file mode 100644 index 0000000000000..ff84b631e3b52 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.md @@ -0,0 +1,5 @@ +Container parameters +==================== + +- `integer`: `12` +- `string`: `Hello world!` *Since symfony/framework-bundle 6.4: The parameter "string" is deprecated.* diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.txt new file mode 100644 index 0000000000000..197f62a8a4112 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.txt @@ -0,0 +1,11 @@ + +Symfony Container Parameters +============================ + + ---------------------------------------- --------------------------------------- +  Parameter   Value  + ---------------------------------------- --------------------------------------- + integer 12 + string Hello world! + (Since symfony/framework-bundle 6.4: The parameter "string" is deprecated.) + ---------------------------------------- --------------------------------------- diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.xml new file mode 100644 index 0000000000000..24ff71e1c46c9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecated_parameters.xml @@ -0,0 +1,5 @@ + + + 12 + Hello world! +