-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DependencyInjection] Make method (setter) autowiring configurable #20167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@@ -662,13 +662,28 @@ public function setAutowiringTypes(array $types) | |||
*/ | |||
public function isAutowired() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I deprecate this method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure it's worth it.
👍 |
The syntax for the XML loader is: <?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="autowire_star" class="Foo" autowire="*" />
<service id="autowire_array" class="Foo">
<autowire>setFoo</autowire>
<autowire>bar</autowire>
</service>
</services>
</container> Does it looks good to everybody? |
64b789a
to
9d4ae07
Compare
$methods = array(); | ||
foreach ($autowired as $methodName) { | ||
if ($reflectionClass->hasMethod($methodName)) { | ||
$methods[] = $reflectionClass->getMethod($methodName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't work when using ['__construct']
right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It works fine: https://3v4l.org/Lr78t
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure but it will add its arguments as a call instead of constructor arguments. And it may create weird bugs and not work at all if there are mandatory constructor arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch I'll update that.
@@ -41,8 +41,8 @@ public function process(ContainerBuilder $container) | |||
try { | |||
$this->container = $container; | |||
foreach ($container->getDefinitions() as $id => $definition) { | |||
if ($definition->isAutowired()) { | |||
$this->completeDefinition($id, $definition); | |||
if (false !== $autowired = $definition->getAutowired()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about keep using $definition->isAutowired()
here (as it stay valid), remove the newly added $autowired
argument from completeDefinition()
and use $autowired = $defintion->getAutowired();
directly into?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It adds some extra method calls for no reason. I prefer to keep it as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current adds an extra argument to completeDefinition
for no reason, it can be done in the method directly as you have the Definition
, and the check is doing exactly what the method call would do thus ... Up to you.
@@ -72,8 +72,10 @@ public static function createResourceForClass(\ReflectionClass $reflectionClass) | |||
$metadata['__construct'] = self::getResourceMetadataForMethod($constructor); | |||
} | |||
|
|||
foreach (self::getSetters($reflectionClass) as $reflectionMethod) { | |||
$metadata[$reflectionMethod->name] = self::getResourceMetadataForMethod($reflectionMethod); | |||
foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@weaverryan can you take a look at this change?
Because it's now possible to autowire any method (not just only setters), I think that there is now other solution than watching all defined methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about allowing masks? (eg set*
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can indeed allow masks in the configuration, but it's not related to this code block (it is not aware of the configuration so it needs to watch all methods anyway).
ping @symfony/deciders. We need to choose between merging this one or reverting setter autowiring before the 3.2 release. |
*/ | ||
public function getMethods(\ReflectionClass $reflectionClass, array $autowired) | ||
{ | ||
if (is_array($autowired)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's always an array as it is typehinted
* | ||
* @return \ReflectionMethod[] | ||
*/ | ||
public function getMethods(\ReflectionClass $reflectionClass, array $autowired) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is meant to be private
The XML loader should probably forbid to use both the autowire attribute and elements at the same time. Stuff like that doesn't make sense: <?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="autowire_star" class="Foo" autowire="*">
<autowire>setFoo</autowire>
<autowire>bar</autowire>
</service>
<service id="autowire_array" class="Foo" autowire="false">
<autowire>setFoo</autowire>
<autowire>bar</autowire>
</service>
</services>
</container> |
|
||
<xsd:simpleType name="boolean_or_star"> | ||
<xsd:restriction base="xsd:string"> | ||
<xsd:pattern value="(%.+%|true|false|\*)" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A boolean is XML schema is also allowed to be 1 and 0. So this might break BC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't a BC break because it was already not using the boolean type of XML schema but the one locally defined. I've just copied, pasted and extended the current type definition to allow using a *
character:
<xsd:simpleType name="boolean">
<xsd:restriction base="xsd:string">
<xsd:pattern value="(%.+%|true|false)" />
</xsd:restriction>
</xsd:simpleType>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, it's to allow parameters. I'm pretty sure allowing dynamic values for autowiring should not be allowed and makes no sense after all. I don't see the point of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by dynamic values? It allows to write:
<service class="Foo" autowire="true"/> <!-- Autowire constructor only (BC) -->
<!-- or -->
<service class="Foo" autowire="*"/> <!-- Autowire constructor + setters -->
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean parameters: <service class="Foo" autowire="%param%"/>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok got it. But we cannot change it because it was already allowed since 2.8 (because it was using the local boolean
type like all others parameters).
So if you explicitly autowire a method that has no arguments it is basically the same as a normal call configuration to this method? I suspect people will then misuse autowiring just to call certain methods since there are now 2 ways to do that. |
@Tobion thanks for the review, comments fixed now.
I think that it's not the same intent:
But again, and as pointed in the doc, I would discourage end users to use setter autowiring. It's a feature designed for creators of RAD frameworks and bundles. |
Fine for me. One more thing. I think we should make the programmatic autowire: true # constructor autowiring
autowire: [__construct, setFoo, setBar] # autowire whitelisted methods only
autowire: '*' # autowire constructor + every setters (following existing rules for setters autowiring) makes sense as @dunglas and @nicolas-grekas pointed out. But using a magic
does not makse sense. So I'd prefer to change the methods on the
The current yaml/xml loaders would just map to this explicit API. |
It will be difficult to make this new API backward compatible with the old one. Currently |
@dunglas what about this approach? $def->setAutowired(false); // no autowiring
$def->setAutorwired(true/**Def::CONSTRUCTOR*/); // constructor autowiring
$def->setAutowired(Def::SETTERS); // setter autowiring
$def->setAutorwired(array('foo')); // whitelist autowiring
$def->setAutorwired(Def::CONSTRUCTOR|Def::SETTERS); // combined autowiring
// optional: combined + whitelist autowiring
$def->setAutorwired(Def::CONSTRUCTOR|Def::SETTERS|Def::WHITELIST, array('foo')); autowire: false # no autowiring
autowire: true | 'constructor' | constant('Def::CONSTRUCTOR') # constructor autowiring
autowire: 'setters' | constant('Def::SETTERS') # setter autowiring
autowire: # whitelist autowiring
- foo
autowire: # combined autowiring
constructor: true
setters: true
# optional: combined + whitelist autowiring
autowire:
constructor: true
setters: true
whitelist:
- foo |
@ro0NL I don't want to change the format for XML and YAML because we already have reached a consensus. Your proposal for the PHP API doesn't fix the issue raised by @Tobion: the Using a bit field isn't the right choice here: you cannot accumulate options. You can only choose one option between constructor autowiring, constructor + setter autowiring or a list of methods to autowire. IMO the current implementation is good enough even if I would prefer accepting only one type for Maybe can we just introduce a |
Fair enough. Just saying im not sure
I'd prefer the latter in terms of less magic, extensibility and verbose configuration. But you're right, we've already reached consensus before. Just thought of this today :) |
a3da23a
to
77c8359
Compare
@@ -24,6 +24,9 @@ | |||
*/ | |||
class AutowirePass implements CompilerPassInterface | |||
{ | |||
/** | |||
* @var ContainerBuilder |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
really required? isn't the type hint on the constructor enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without it PHPStorm's autocompletion doesn't work, but I can remove it :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no constructor btw (it's why it's required).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh right, the function I looked at is process
ok then
* | ||
* @throws RuntimeException | ||
*/ | ||
private function completeDefinition($id, Definition $definition, $autowired) | ||
private function completeDefinition($id, Definition $definition, array $autowired = array('__construct')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since this is private, let's make the arg mandatory to make the calling code easier to read?
@@ -41,7 +44,7 @@ public function process(ContainerBuilder $container) | |||
try { | |||
$this->container = $container; | |||
foreach ($container->getDefinitions() as $id => $definition) { | |||
if (false !== $autowired = $definition->getAutowired()) { | |||
if (!empty($autowired = $definition->getAutowired())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a bug fan of using empty
. I'd prefer if ($autowired = $definition->getAutowired()) {
Final thoughts; my vote goes to setAutowire($boolOrArray)
Wouldn't be mine: union types require higher cyclomatic complexity. Having
simple types looks better to me.
|
Perhaps should be avoided, but in this case (to me) it makes most sense from a configuring perspective. Eventually a YAML/XML/PHP definition represents the same thing, and therefore should have similar (if not the same) schema/API imo. edit: on 2nd thought :):) maybe choose the desired PHP API now (eg. |
My vote would be for reverting #17608 and merge this one for 3.3. |
I agree, it's too late for 3.2. |
… add setter injection support (dunglas)" (nicolas-grekas) This PR was merged into the 3.2-dev branch. Discussion ---------- Revert "feature #17608 [DependencyInjection] Autowiring: add setter injection support (dunglas)" | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - This reverts commit 7eab6b9, reversing changes made to 35f201f. As discussed in #20167 Commits ------- bf91eda Revert "feature #17608 [DependencyInjection] Autowiring: add setter injection support (dunglas)"
9d7e8b3
to
48ab62f
Compare
I've rebased the PR and updated method's names. I've added two new methods However I've kept |
@@ -239,6 +239,19 @@ private function parseDefinition(\DOMElement $service, $file) | |||
$definition->addAutowiringType($type->textContent); | |||
} | |||
|
|||
$autowireTags = array(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$autowiredMethods
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to keep the current name here because this var contains the value of the XML tag.
@@ -41,8 +44,8 @@ public function process(ContainerBuilder $container) | |||
try { | |||
$this->container = $container; | |||
foreach ($container->getDefinitions() as $id => $definition) { | |||
if ($definition->isAutowired()) { | |||
$this->completeDefinition($id, $definition); | |||
if ($autowired = $definition->getAutowiredMethods()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$autowiredMethods
?
* | ||
* @throws RuntimeException | ||
*/ | ||
private function completeDefinition($id, Definition $definition) | ||
private function completeDefinition($id, Definition $definition, array $autowired) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$autowiredMethods
?
$found = array(); | ||
$regexList = array(); | ||
foreach ($autowired as $pattern) { | ||
$regexList[] = '/^'.str_replace('\*', '.*', preg_quote($pattern, '/')).'$/i'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps use .+
, but it depends if set*
should match set()
. Not sure :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say that set()
is ok but I'm not sure too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
*
comes from glob patterns, and there, *
=> /.*/
, we shouldn't be the one redefining old semantics people are used to since years.
* | ||
* @return Definition The current instance | ||
*/ | ||
public function setAutowired($autowiredMethods) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$autowired
*/ | ||
public function setAutowired($autowiredMethods) | ||
{ | ||
$this->autowiredMethods = $autowiredMethods ? array('__construct') : array(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would call setAutowiredMethods
here..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? This method is just a mutator, we can avoid an extra function call and potential unwanted side effects in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough.
$autowireTags[] = $type->textContent; | ||
} | ||
|
||
if (!empty($autowireTags)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (0 !== count($autowiredTags)) {
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even better: if ($autowiredTags) {
I'm fighting against the idea that using count() provides any type information. It does not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right.. got confused by !
😞
* | ||
* @param string $id | ||
* @param \ReflectionClass $reflectionClass | ||
* @param string[] $autowired |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$autowiredMethods
?
{ | ||
if ($isConstructor = $reflectionMethod->isConstructor()) { | ||
$arguments = $definition->getArguments(); | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about removing the else
?
$arguments = array();
if ($isConstructor = $reflectionMethod->isConstructor()) {
$arguments = $definition->getArguments();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It adds an extra assignation. Not sure it worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you want, that's just a detail after all ☺
@@ -111,7 +180,11 @@ private function completeDefinition($id, Definition $definition) | |||
if (!$typeHint = $parameter->getClass()) { | |||
// no default value? Then fail | |||
if (!$parameter->isOptional()) { | |||
throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id)); | |||
if ($isConstructor) { | |||
throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about reversing the if
condition here to make the exception message more readable?
if (!$isConstructor) {
return;
}
// ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I prefer using non-negative condition when possible (and it's possible here).
$arguments = array(); | ||
} | ||
|
||
$addMethodCall = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a small comment here? I don't think it's easy to understand what contains this variable without looking deeply the code.
Maybe // Whether the method should be added to the definition as a call
.
At the same time, I don't remember if it's written somewhere in the code what's this pass behavior (e.g. only autowire methods when at least one if their arguments is not to its default value), it could be worth adding it if not.
$pass = new AutowirePass(); | ||
$pass->process($container); | ||
|
||
$this->assertEquals(array('Symfony\Component\DependencyInjection\Compiler\AutowirePass: Autowiring\'s patterns "not", "exist*" for service "foo" don\'t match any method.'), $container->getCompiler()->getLog()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AutowirePass::class
?
@@ -290,6 +290,16 @@ public function testAutowired() | |||
$this->assertFalse($def->isAutowired()); | |||
$def->setAutowired(true); | |||
$this->assertTrue($def->isAutowired()); | |||
$this->assertEquals(array('__construct'), $def->getAutowiredMethods()); | |||
|
|||
$def->setAutowired(array('foo')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setAutowiredMethods
?
$found = array(); | ||
$regexList = array(); | ||
foreach ($autowiredMethods as $pattern) { | ||
$regexList[] = '/^'.str_replace('\*', '.*', preg_quote($pattern, '/')).'$/i'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be consider allowing some other basic glob patterns, ie. ? => .
Could be convenient in some cases...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need IMHO
25de8df
to
fe02a59
Compare
e981154
to
6dd53c7
Compare
👍 for merging asap, so that we can play with the feat asap and tweak it if needed in the coming months. |
} catch (RuntimeException $e) { | ||
if ($parameter->allowsNull()) { | ||
$value = null; | ||
} elseif ($parameter->isDefaultValueAvailable()) { | ||
$value = $parameter->getDefaultValue(); | ||
} else { | ||
throw $e; | ||
// The exception code is set to 1 if the exception must be thrown even if it's a setter | ||
if (1 === $e->getCode() || $isConstructor) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use a specific getter/setter instead of a magic 1 value for the code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done (but I find the use of a specific code less intrusive than using accessors, here we add methods to a generic exception for an implementation detail of the AutowirePass
).
* Allowed values: | ||
* - true: constructor autowiring, same as $this->setAutowiredMethods(array('__construct')) | ||
* - false: no autowiring, same as $this->setAutowiredMethods(array()) | ||
* | ||
* @param bool $autowired | ||
* | ||
* @return Definition The current instance | ||
*/ | ||
public function setAutowired($autowired) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one could be deprecated, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I propose to deprecate it in another PR (if it's worth it) because we need to determine how we deal with #20648
{ | ||
public function setFoo(Foo $foo) | ||
{ | ||
// should be called |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about throwing an exception here instead of the comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This setter should be called (and shouldn't throw anything).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nevermind :)
Thank you @dunglas. |
…configurable (dunglas) This PR was merged into the 3.3-dev branch. Discussion ---------- [DependencyInjection] Make method (setter) autowiring configurable | Q | A | | --- | --- | | Branch? | master | | Bug fix? | no | | New feature? | yes | | BC breaks? | no | | Deprecations? | maybe? | | Tests pass? | yes | | Fixed tickets | #19631 | | License | MIT | | Doc PR | symfony/symfony-docs#7041 | Follow up of #19631. Implements #19631 (comment): Edit: the last supported format: ``` yaml services: foo: class: Foo autowire: ['__construct', 'set*'] # Autowire constructor and all setters autowire: true # Converted by loaders in `autowire: ['__construct']` for BC autowire: ['foo', 'bar'] # Autowire only `foo` and `bar` methods ``` Outdated: ``` yaml autowire: true # constructor autowiring autowire: [__construct, setFoo, setBar] # autowire whitelisted methods only autowire: '*' # autowire constructor + every setters (following existing rules for setters autowiring) ``` - [x] Allow to specify the list of methods in the XML loader - [x] Add tests for the YAML loader Commits ------- 6dd53c7 [DependencyInjection] Introduce method injection for autowiring
Follow up of #19631. Implements #19631 (comment):
Edit: the last supported format:
Outdated: