Merge branch '4.1'
* 4.1: Undeprecate the single-colon notation for controllers Command::addOption should allow int in $default Update symfony links to https [Form] Fixed keeping hash of equal \DateTimeInterface on submit [PhpUnitBridge] Fix typo [Routing] generate(null) should throw an exception [Form] Minor fixes in docs and cs [Workflow] Made code simpler [Config] Unset key during normalization [Form] Fixed empty data for compound date types invalidate forms on transformation failures [FrameworkBundle] fixed guard event names for transitions method buildTransitionBlockerList returns TransitionBlockerList of expected transition [FrameworkBundle] fixed guard event names for transitions [PropertyAccessor] Fix unable to write to singular property using setter while plural adder/remover exist
This commit is contained in:
commit
d3ba3fb87d
@ -126,7 +126,7 @@ Form
|
|||||||
FrameworkBundle
|
FrameworkBundle
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
* Removed support for `bundle:controller:action` and `service:action` syntaxes to reference controllers. Use `serviceOrFqcn::method`
|
* Removed support for `bundle:controller:action` syntax to reference controllers. Use `serviceOrFqcn::method`
|
||||||
instead where `serviceOrFqcn` is either the service ID when using controllers as services or the FQCN of the controller.
|
instead where `serviceOrFqcn` is either the service ID when using controllers as services or the FQCN of the controller.
|
||||||
|
|
||||||
Before:
|
Before:
|
||||||
@ -136,11 +136,6 @@ FrameworkBundle
|
|||||||
path: /
|
path: /
|
||||||
defaults:
|
defaults:
|
||||||
_controller: FrameworkBundle:Redirect:redirect
|
_controller: FrameworkBundle:Redirect:redirect
|
||||||
|
|
||||||
service_controller:
|
|
||||||
path: /
|
|
||||||
defaults:
|
|
||||||
_controller: app.my_controller:myAction
|
|
||||||
```
|
```
|
||||||
|
|
||||||
After:
|
After:
|
||||||
@ -150,11 +145,6 @@ FrameworkBundle
|
|||||||
path: /
|
path: /
|
||||||
defaults:
|
defaults:
|
||||||
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
|
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
|
||||||
|
|
||||||
service_controller:
|
|
||||||
path: /
|
|
||||||
defaults:
|
|
||||||
_controller: app.my_controller::myAction
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* Removed `Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser`.
|
* Removed `Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser`.
|
||||||
|
@ -33,7 +33,7 @@ class DeprecationErrorHandler
|
|||||||
* - use "/some-regexp/" to stop the test suite whenever a deprecation
|
* - use "/some-regexp/" to stop the test suite whenever a deprecation
|
||||||
* message matches the given regular expression;
|
* message matches the given regular expression;
|
||||||
* - use a number to define the upper bound of allowed deprecations,
|
* - use a number to define the upper bound of allowed deprecations,
|
||||||
* making the test suite fail whenever more notices are trigerred.
|
* making the test suite fail whenever more notices are triggered.
|
||||||
*
|
*
|
||||||
* @param int|string|false $mode The reporting mode, defaults to not allowing any deprecations
|
* @param int|string|false $mode The reporting mode, defaults to not allowing any deprecations
|
||||||
*/
|
*/
|
||||||
|
@ -507,6 +507,7 @@ class FrameworkExtension extends Extension
|
|||||||
|
|
||||||
foreach ($config['workflows'] as $name => $workflow) {
|
foreach ($config['workflows'] as $name => $workflow) {
|
||||||
$type = $workflow['type'];
|
$type = $workflow['type'];
|
||||||
|
$workflowId = sprintf('%s.%s', $type, $name);
|
||||||
|
|
||||||
// Process Metadata (workflow + places (transition is done in the "create transition" block))
|
// Process Metadata (workflow + places (transition is done in the "create transition" block))
|
||||||
$metadataStoreDefinition = new Definition(Workflow\Metadata\InMemoryMetadataStore::class, array(array(), array(), null));
|
$metadataStoreDefinition = new Definition(Workflow\Metadata\InMemoryMetadataStore::class, array(array(), array(), null));
|
||||||
@ -525,11 +526,25 @@ class FrameworkExtension extends Extension
|
|||||||
|
|
||||||
// Create transitions
|
// Create transitions
|
||||||
$transitions = array();
|
$transitions = array();
|
||||||
|
$guardsConfiguration = array();
|
||||||
$transitionsMetadataDefinition = new Definition(\SplObjectStorage::class);
|
$transitionsMetadataDefinition = new Definition(\SplObjectStorage::class);
|
||||||
|
// Global transition counter per workflow
|
||||||
|
$transitionCounter = 0;
|
||||||
foreach ($workflow['transitions'] as $transition) {
|
foreach ($workflow['transitions'] as $transition) {
|
||||||
if ('workflow' === $type) {
|
if ('workflow' === $type) {
|
||||||
$transitionDefinition = new Definition(Workflow\Transition::class, array($transition['name'], $transition['from'], $transition['to']));
|
$transitionDefinition = new Definition(Workflow\Transition::class, array($transition['name'], $transition['from'], $transition['to']));
|
||||||
$transitions[] = $transitionDefinition;
|
$transitionDefinition->setPublic(false);
|
||||||
|
$transitionId = sprintf('%s.transition.%s', $workflowId, $transitionCounter++);
|
||||||
|
$container->setDefinition($transitionId, $transitionDefinition);
|
||||||
|
$transitions[] = new Reference($transitionId);
|
||||||
|
if (isset($transition['guard'])) {
|
||||||
|
$configuration = new Definition(Workflow\EventListener\GuardExpression::class);
|
||||||
|
$configuration->addArgument(new Reference($transitionId));
|
||||||
|
$configuration->addArgument($transition['guard']);
|
||||||
|
$configuration->setPublic(false);
|
||||||
|
$eventName = sprintf('workflow.%s.guard.%s', $name, $transition['name']);
|
||||||
|
$guardsConfiguration[$eventName][] = $configuration;
|
||||||
|
}
|
||||||
if ($transition['metadata']) {
|
if ($transition['metadata']) {
|
||||||
$transitionsMetadataDefinition->addMethodCall('attach', array(
|
$transitionsMetadataDefinition->addMethodCall('attach', array(
|
||||||
$transitionDefinition,
|
$transitionDefinition,
|
||||||
@ -540,7 +555,18 @@ class FrameworkExtension extends Extension
|
|||||||
foreach ($transition['from'] as $from) {
|
foreach ($transition['from'] as $from) {
|
||||||
foreach ($transition['to'] as $to) {
|
foreach ($transition['to'] as $to) {
|
||||||
$transitionDefinition = new Definition(Workflow\Transition::class, array($transition['name'], $from, $to));
|
$transitionDefinition = new Definition(Workflow\Transition::class, array($transition['name'], $from, $to));
|
||||||
$transitions[] = $transitionDefinition;
|
$transitionDefinition->setPublic(false);
|
||||||
|
$transitionId = sprintf('%s.transition.%s', $workflowId, $transitionCounter++);
|
||||||
|
$container->setDefinition($transitionId, $transitionDefinition);
|
||||||
|
$transitions[] = new Reference($transitionId);
|
||||||
|
if (isset($transition['guard'])) {
|
||||||
|
$configuration = new Definition(Workflow\EventListener\GuardExpression::class);
|
||||||
|
$configuration->addArgument(new Reference($transitionId));
|
||||||
|
$configuration->addArgument($transition['guard']);
|
||||||
|
$configuration->setPublic(false);
|
||||||
|
$eventName = sprintf('workflow.%s.guard.%s', $name, $transition['name']);
|
||||||
|
$guardsConfiguration[$eventName][] = $configuration;
|
||||||
|
}
|
||||||
if ($transition['metadata']) {
|
if ($transition['metadata']) {
|
||||||
$transitionsMetadataDefinition->addMethodCall('attach', array(
|
$transitionsMetadataDefinition->addMethodCall('attach', array(
|
||||||
$transitionDefinition,
|
$transitionDefinition,
|
||||||
@ -582,7 +608,6 @@ class FrameworkExtension extends Extension
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create Workflow
|
// Create Workflow
|
||||||
$workflowId = sprintf('%s.%s', $type, $name);
|
|
||||||
$workflowDefinition = new ChildDefinition(sprintf('%s.abstract', $type));
|
$workflowDefinition = new ChildDefinition(sprintf('%s.abstract', $type));
|
||||||
$workflowDefinition->replaceArgument(0, new Reference(sprintf('%s.definition', $workflowId)));
|
$workflowDefinition->replaceArgument(0, new Reference(sprintf('%s.definition', $workflowId)));
|
||||||
if (isset($markingStoreDefinition)) {
|
if (isset($markingStoreDefinition)) {
|
||||||
@ -619,16 +644,7 @@ class FrameworkExtension extends Extension
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add Guard Listener
|
// Add Guard Listener
|
||||||
$guard = new Definition(Workflow\EventListener\GuardListener::class);
|
if ($guardsConfiguration) {
|
||||||
$guard->setPrivate(true);
|
|
||||||
$configuration = array();
|
|
||||||
foreach ($workflow['transitions'] as $config) {
|
|
||||||
$transitionName = $config['name'];
|
|
||||||
|
|
||||||
if (!isset($config['guard'])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!class_exists(ExpressionLanguage::class)) {
|
if (!class_exists(ExpressionLanguage::class)) {
|
||||||
throw new LogicException('Cannot guard workflows as the ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
|
throw new LogicException('Cannot guard workflows as the ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
|
||||||
}
|
}
|
||||||
@ -637,13 +653,11 @@ class FrameworkExtension extends Extension
|
|||||||
throw new LogicException('Cannot guard workflows as the Security component is not installed. Try running "composer require symfony/security".');
|
throw new LogicException('Cannot guard workflows as the Security component is not installed. Try running "composer require symfony/security".');
|
||||||
}
|
}
|
||||||
|
|
||||||
$eventName = sprintf('workflow.%s.guard.%s', $name, $transitionName);
|
$guard = new Definition(Workflow\EventListener\GuardListener::class);
|
||||||
$guard->addTag('kernel.event_listener', array('event' => $eventName, 'method' => 'onTransition'));
|
$guard->setPrivate(true);
|
||||||
$configuration[$eventName] = $config['guard'];
|
|
||||||
}
|
|
||||||
if ($configuration) {
|
|
||||||
$guard->setArguments(array(
|
$guard->setArguments(array(
|
||||||
$configuration,
|
$guardsConfiguration,
|
||||||
new Reference('workflow.security.expression_language'),
|
new Reference('workflow.security.expression_language'),
|
||||||
new Reference('security.token_storage'),
|
new Reference('security.token_storage'),
|
||||||
new Reference('security.authorization_checker'),
|
new Reference('security.authorization_checker'),
|
||||||
@ -651,6 +665,9 @@ class FrameworkExtension extends Extension
|
|||||||
new Reference('security.role_hierarchy'),
|
new Reference('security.role_hierarchy'),
|
||||||
new Reference('validator', ContainerInterface::NULL_ON_INVALID_REFERENCE),
|
new Reference('validator', ContainerInterface::NULL_ON_INVALID_REFERENCE),
|
||||||
));
|
));
|
||||||
|
foreach ($guardsConfiguration as $eventName => $config) {
|
||||||
|
$guard->addTag('kernel.event_listener', array('event' => $eventName, 'method' => 'onTransition'));
|
||||||
|
}
|
||||||
|
|
||||||
$container->setDefinition(sprintf('%s.listener.guard', $workflowId), $guard);
|
$container->setDefinition(sprintf('%s.listener.guard', $workflowId), $guard);
|
||||||
$container->setParameter('workflow.has_guard_listeners', true);
|
$container->setParameter('workflow.has_guard_listeners', true);
|
||||||
|
@ -70,6 +70,11 @@
|
|||||||
<argument type="service" id="form.choice_list_factory"/>
|
<argument type="service" id="form.choice_list_factory"/>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service id="form.type_extension.form.transformation_failure_handling" class="Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension">
|
||||||
|
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
|
||||||
|
<argument type="service" id="translator" on-invalid="ignore" />
|
||||||
|
</service>
|
||||||
|
|
||||||
<!-- FormTypeHttpFoundationExtension -->
|
<!-- FormTypeHttpFoundationExtension -->
|
||||||
<service id="form.type_extension.form.http_foundation" class="Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension">
|
<service id="form.type_extension.form.http_foundation" class="Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension">
|
||||||
<argument type="service" id="form.type_extension.form.request_handler" />
|
<argument type="service" id="form.type_extension.form.request_handler" />
|
||||||
|
@ -314,6 +314,7 @@
|
|||||||
<xsd:element name="from" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
|
<xsd:element name="from" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
|
||||||
<xsd:element name="to" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
|
<xsd:element name="to" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
|
||||||
<xsd:element name="metadata" type="metadata" minOccurs="0" maxOccurs="unbounded" />
|
<xsd:element name="metadata" type="metadata" minOccurs="0" maxOccurs="unbounded" />
|
||||||
|
<xsd:element name="guard" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
|
||||||
</xsd:sequence>
|
</xsd:sequence>
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
</xsd:complexType>
|
</xsd:complexType>
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest;
|
||||||
|
|
||||||
|
$container->loadFromExtension('framework', array(
|
||||||
|
'workflows' => array(
|
||||||
|
'article' => array(
|
||||||
|
'type' => 'workflow',
|
||||||
|
'marking_store' => array(
|
||||||
|
'type' => 'multiple_state',
|
||||||
|
),
|
||||||
|
'supports' => array(
|
||||||
|
FrameworkExtensionTest::class,
|
||||||
|
),
|
||||||
|
'initial_place' => 'draft',
|
||||||
|
'places' => array(
|
||||||
|
'draft',
|
||||||
|
'wait_for_journalist',
|
||||||
|
'approved_by_journalist',
|
||||||
|
'wait_for_spellchecker',
|
||||||
|
'approved_by_spellchecker',
|
||||||
|
'published',
|
||||||
|
),
|
||||||
|
'transitions' => array(
|
||||||
|
'request_review' => array(
|
||||||
|
'from' => 'draft',
|
||||||
|
'to' => array('wait_for_journalist', 'wait_for_spellchecker'),
|
||||||
|
),
|
||||||
|
'journalist_approval' => array(
|
||||||
|
'from' => 'wait_for_journalist',
|
||||||
|
'to' => 'approved_by_journalist',
|
||||||
|
),
|
||||||
|
'spellchecker_approval' => array(
|
||||||
|
'from' => 'wait_for_spellchecker',
|
||||||
|
'to' => 'approved_by_spellchecker',
|
||||||
|
),
|
||||||
|
'publish' => array(
|
||||||
|
'from' => array('approved_by_journalist', 'approved_by_spellchecker'),
|
||||||
|
'to' => 'published',
|
||||||
|
'guard' => '!!true',
|
||||||
|
),
|
||||||
|
'publish_editor_in_chief' => array(
|
||||||
|
'name' => 'publish',
|
||||||
|
'from' => 'draft',
|
||||||
|
'to' => 'published',
|
||||||
|
'guard' => '!!false',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
|
||||||
|
<container xmlns="http://symfony.com/schema/dic/services"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:framework="http://symfony.com/schema/dic/symfony"
|
||||||
|
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
|
||||||
|
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
|
||||||
|
|
||||||
|
<framework:config>
|
||||||
|
<framework:workflow name="article" type="workflow" initial-place="draft">
|
||||||
|
<framework:marking-store type="multiple_state">
|
||||||
|
<framework:argument>a</framework:argument>
|
||||||
|
<framework:argument>a</framework:argument>
|
||||||
|
</framework:marking-store>
|
||||||
|
<framework:support>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest</framework:support>
|
||||||
|
<framework:place>draft</framework:place>
|
||||||
|
<framework:place>wait_for_journalist</framework:place>
|
||||||
|
<framework:place>approved_by_journalist</framework:place>
|
||||||
|
<framework:place>wait_for_spellchecker</framework:place>
|
||||||
|
<framework:place>approved_by_spellchecker</framework:place>
|
||||||
|
<framework:place>published</framework:place>
|
||||||
|
<framework:transition name="request_review">
|
||||||
|
<framework:from>draft</framework:from>
|
||||||
|
<framework:to>wait_for_journalist</framework:to>
|
||||||
|
<framework:to>wait_for_spellchecker</framework:to>
|
||||||
|
</framework:transition>
|
||||||
|
<framework:transition name="journalist_approval">
|
||||||
|
<framework:from>wait_for_journalist</framework:from>
|
||||||
|
<framework:to>approved_by_journalist</framework:to>
|
||||||
|
</framework:transition>
|
||||||
|
<framework:transition name="spellchecker_approval">
|
||||||
|
<framework:from>wait_for_spellchecker</framework:from>
|
||||||
|
<framework:to>approved_by_spellchecker</framework:to>
|
||||||
|
</framework:transition>
|
||||||
|
<framework:transition name="publish">
|
||||||
|
<framework:from>approved_by_journalist</framework:from>
|
||||||
|
<framework:from>approved_by_spellchecker</framework:from>
|
||||||
|
<framework:to>published</framework:to>
|
||||||
|
<framework:guard>!!true</framework:guard>
|
||||||
|
</framework:transition>
|
||||||
|
<framework:transition name="publish">
|
||||||
|
<framework:from>draft</framework:from>
|
||||||
|
<framework:to>published</framework:to>
|
||||||
|
<framework:guard>!!false</framework:guard>
|
||||||
|
</framework:transition>
|
||||||
|
</framework:workflow>
|
||||||
|
</framework:config>
|
||||||
|
</container>
|
@ -0,0 +1,35 @@
|
|||||||
|
framework:
|
||||||
|
workflows:
|
||||||
|
article:
|
||||||
|
type: workflow
|
||||||
|
marking_store:
|
||||||
|
type: multiple_state
|
||||||
|
supports:
|
||||||
|
- Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest
|
||||||
|
initial_place: draft
|
||||||
|
places:
|
||||||
|
- draft
|
||||||
|
- wait_for_journalist
|
||||||
|
- approved_by_journalist
|
||||||
|
- wait_for_spellchecker
|
||||||
|
- approved_by_spellchecker
|
||||||
|
- published
|
||||||
|
transitions:
|
||||||
|
request_review:
|
||||||
|
from: [draft]
|
||||||
|
to: [wait_for_journalist, wait_for_spellchecker]
|
||||||
|
journalist_approval:
|
||||||
|
from: [wait_for_journalist]
|
||||||
|
to: [approved_by_journalist]
|
||||||
|
spellchecker_approval:
|
||||||
|
from: [wait_for_spellchecker]
|
||||||
|
to: [approved_by_spellchecker]
|
||||||
|
publish:
|
||||||
|
from: [approved_by_journalist, approved_by_spellchecker]
|
||||||
|
to: [published]
|
||||||
|
guard: "!!true"
|
||||||
|
publish_editor_in_chief:
|
||||||
|
name: publish
|
||||||
|
from: [draft]
|
||||||
|
to: [published]
|
||||||
|
guard: "!!false"
|
@ -323,14 +323,84 @@ abstract class FrameworkExtensionTest extends TestCase
|
|||||||
|
|
||||||
$this->assertCount(5, $transitions);
|
$this->assertCount(5, $transitions);
|
||||||
|
|
||||||
$this->assertSame('request_review', $transitions[0]->getArgument(0));
|
$this->assertSame('workflow.article.transition.0', (string) $transitions[0]);
|
||||||
$this->assertSame('journalist_approval', $transitions[1]->getArgument(0));
|
$this->assertSame(array(
|
||||||
$this->assertSame('spellchecker_approval', $transitions[2]->getArgument(0));
|
'request_review',
|
||||||
$this->assertSame('publish', $transitions[3]->getArgument(0));
|
array(
|
||||||
$this->assertSame('publish', $transitions[4]->getArgument(0));
|
'draft',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'wait_for_journalist', 'wait_for_spellchecker',
|
||||||
|
),
|
||||||
|
), $container->getDefinition($transitions[0])->getArguments());
|
||||||
|
|
||||||
$this->assertSame(array('approved_by_journalist', 'approved_by_spellchecker'), $transitions[3]->getArgument(1));
|
$this->assertSame('workflow.article.transition.1', (string) $transitions[1]);
|
||||||
$this->assertSame(array('draft'), $transitions[4]->getArgument(1));
|
$this->assertSame(array(
|
||||||
|
'journalist_approval',
|
||||||
|
array(
|
||||||
|
'wait_for_journalist',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'approved_by_journalist',
|
||||||
|
),
|
||||||
|
), $container->getDefinition($transitions[1])->getArguments());
|
||||||
|
|
||||||
|
$this->assertSame('workflow.article.transition.2', (string) $transitions[2]);
|
||||||
|
$this->assertSame(array(
|
||||||
|
'spellchecker_approval',
|
||||||
|
array(
|
||||||
|
'wait_for_spellchecker',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'approved_by_spellchecker',
|
||||||
|
),
|
||||||
|
), $container->getDefinition($transitions[2])->getArguments());
|
||||||
|
|
||||||
|
$this->assertSame('workflow.article.transition.3', (string) $transitions[3]);
|
||||||
|
$this->assertSame(array(
|
||||||
|
'publish',
|
||||||
|
array(
|
||||||
|
'approved_by_journalist',
|
||||||
|
'approved_by_spellchecker',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'published',
|
||||||
|
),
|
||||||
|
), $container->getDefinition($transitions[3])->getArguments());
|
||||||
|
|
||||||
|
$this->assertSame('workflow.article.transition.4', (string) $transitions[4]);
|
||||||
|
$this->assertSame(array(
|
||||||
|
'publish',
|
||||||
|
array(
|
||||||
|
'draft',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'published',
|
||||||
|
),
|
||||||
|
), $container->getDefinition($transitions[4])->getArguments());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGuardExpressions()
|
||||||
|
{
|
||||||
|
$container = $this->createContainerFromFile('workflow_with_guard_expression');
|
||||||
|
|
||||||
|
$this->assertTrue($container->hasDefinition('workflow.article.listener.guard'), 'Workflow guard listener is registered as a service');
|
||||||
|
$this->assertTrue($container->hasParameter('workflow.has_guard_listeners'), 'Workflow guard listeners parameter exists');
|
||||||
|
$this->assertTrue(true === $container->getParameter('workflow.has_guard_listeners'), 'Workflow guard listeners parameter is enabled');
|
||||||
|
$guardDefinition = $container->getDefinition('workflow.article.listener.guard');
|
||||||
|
$this->assertSame(array(
|
||||||
|
array(
|
||||||
|
'event' => 'workflow.article.guard.publish',
|
||||||
|
'method' => 'onTransition',
|
||||||
|
),
|
||||||
|
), $guardDefinition->getTag('kernel.event_listener'));
|
||||||
|
$guardsConfiguration = $guardDefinition->getArgument(0);
|
||||||
|
$this->assertTrue(1 === \count($guardsConfiguration), 'Workflow guard configuration contains one element per transition name');
|
||||||
|
$transitionGuardExpressions = $guardsConfiguration['workflow.article.guard.publish'];
|
||||||
|
$this->assertSame('workflow.article.transition.3', (string) $transitionGuardExpressions[0]->getArgument(0));
|
||||||
|
$this->assertSame('!!true', $transitionGuardExpressions[0]->getArgument(1));
|
||||||
|
$this->assertSame('workflow.article.transition.4', (string) $transitionGuardExpressions[1]->getArgument(0));
|
||||||
|
$this->assertSame('!!false', $transitionGuardExpressions[1]->getArgument(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testWorkflowServicesCanBeEnabled()
|
public function testWorkflowServicesCanBeEnabled()
|
||||||
|
@ -88,7 +88,7 @@
|
|||||||
<div class="sf-toolbar-info-piece">
|
<div class="sf-toolbar-info-piece">
|
||||||
<b>Help</b>
|
<b>Help</b>
|
||||||
<span>
|
<span>
|
||||||
<a href="http://symfony.com/support">
|
<a href="https://symfony.com/support">
|
||||||
Symfony Support Channels
|
Symfony Support Channels
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
@ -292,7 +292,10 @@ class ArrayNode extends BaseNode implements PrototypeNodeInterface
|
|||||||
$normalized = array();
|
$normalized = array();
|
||||||
foreach ($value as $name => $val) {
|
foreach ($value as $name => $val) {
|
||||||
if (isset($this->children[$name])) {
|
if (isset($this->children[$name])) {
|
||||||
$normalized[$name] = $this->children[$name]->normalize($val);
|
try {
|
||||||
|
$normalized[$name] = $this->children[$name]->normalize($val);
|
||||||
|
} catch (UnsetKeyException $e) {
|
||||||
|
}
|
||||||
unset($value[$name]);
|
unset($value[$name]);
|
||||||
} elseif (!$this->removeExtraKeys) {
|
} elseif (!$this->removeExtraKeys) {
|
||||||
$normalized[$name] = $val;
|
$normalized[$name] = $val;
|
||||||
|
@ -174,7 +174,7 @@ class ExprBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a closure marking the value as invalid at validation time.
|
* Sets a closure marking the value as invalid at processing time.
|
||||||
*
|
*
|
||||||
* if you want to add the value of the node in your message just use a %s placeholder.
|
* if you want to add the value of the node in your message just use a %s placeholder.
|
||||||
*
|
*
|
||||||
@ -192,7 +192,7 @@ class ExprBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a closure unsetting this key of the array at validation time.
|
* Sets a closure unsetting this key of the array at processing time.
|
||||||
*
|
*
|
||||||
* @return $this
|
* @return $this
|
||||||
*
|
*
|
||||||
|
@ -232,6 +232,25 @@ class ArrayNodeDefinitionTest extends TestCase
|
|||||||
$this->assertFalse($this->getField($node, 'normalizeKeys'));
|
$this->assertFalse($this->getField($node, 'normalizeKeys'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testUnsetChild()
|
||||||
|
{
|
||||||
|
$node = new ArrayNodeDefinition('root');
|
||||||
|
$node
|
||||||
|
->children()
|
||||||
|
->scalarNode('value')
|
||||||
|
->beforeNormalization()
|
||||||
|
->ifTrue(function ($value) {
|
||||||
|
return empty($value);
|
||||||
|
})
|
||||||
|
->thenUnset()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
;
|
||||||
|
|
||||||
|
$this->assertSame(array(), $node->getNode()->normalize(array('value' => null)));
|
||||||
|
}
|
||||||
|
|
||||||
public function testPrototypeVariable()
|
public function testPrototypeVariable()
|
||||||
{
|
{
|
||||||
$node = new ArrayNodeDefinition('root');
|
$node = new ArrayNodeDefinition('root');
|
||||||
|
@ -379,11 +379,11 @@ class Command
|
|||||||
/**
|
/**
|
||||||
* Adds an option.
|
* Adds an option.
|
||||||
*
|
*
|
||||||
* @param string $name The option name
|
* @param string $name The option name
|
||||||
* @param string|array $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
|
* @param string|array $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
|
||||||
* @param int|null $mode The option mode: One of the VALUE_* constants
|
* @param int|null $mode The option mode: One of the VALUE_* constants
|
||||||
* @param string $description A description text
|
* @param string $description A description text
|
||||||
* @param string|string[]|bool|null $default The default value (must be null for self::VALUE_NONE)
|
* @param string|string[]|int|bool|null $default The default value (must be null for self::VALUE_NONE)
|
||||||
*
|
*
|
||||||
* @throws InvalidArgumentException If option mode is invalid or incompatible
|
* @throws InvalidArgumentException If option mode is invalid or incompatible
|
||||||
*
|
*
|
||||||
|
@ -16,8 +16,10 @@ use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
|
|||||||
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
|
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
|
||||||
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
|
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
|
||||||
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
|
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension;
|
||||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||||
|
use Symfony\Component\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the main form extension, which loads the core functionality.
|
* Represents the main form extension, which loads the core functionality.
|
||||||
@ -28,11 +30,13 @@ class CoreExtension extends AbstractExtension
|
|||||||
{
|
{
|
||||||
private $propertyAccessor;
|
private $propertyAccessor;
|
||||||
private $choiceListFactory;
|
private $choiceListFactory;
|
||||||
|
private $translator;
|
||||||
|
|
||||||
public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
|
public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null, TranslatorInterface $translator = null)
|
||||||
{
|
{
|
||||||
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
|
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
|
||||||
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
|
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
|
||||||
|
$this->translator = $translator;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function loadTypes()
|
protected function loadTypes()
|
||||||
@ -74,4 +78,11 @@ class CoreExtension extends AbstractExtension
|
|||||||
new Type\ColorType(),
|
new Type\ColorType(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function loadTypeExtensions()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
new TransformationFailureExtension($this->translator),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,16 +73,17 @@ class PropertyPathMapper implements DataMapperInterface
|
|||||||
// Write-back is disabled if the form is not synchronized (transformation failed),
|
// Write-back is disabled if the form is not synchronized (transformation failed),
|
||||||
// if the form was not submitted and if the form is disabled (modification not allowed)
|
// if the form was not submitted and if the form is disabled (modification not allowed)
|
||||||
if (null !== $propertyPath && $config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled()) {
|
if (null !== $propertyPath && $config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled()) {
|
||||||
// If the field is of type DateTime and the data is the same skip the update to
|
$propertyValue = $form->getData();
|
||||||
|
// If the field is of type DateTimeInterface and the data is the same skip the update to
|
||||||
// keep the original object hash
|
// keep the original object hash
|
||||||
if ($form->getData() instanceof \DateTime && $form->getData() == $this->propertyAccessor->getValue($data, $propertyPath)) {
|
if ($propertyValue instanceof \DateTimeInterface && $propertyValue == $this->propertyAccessor->getValue($data, $propertyPath)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the data is identical to the value in $data, we are
|
// If the data is identical to the value in $data, we are
|
||||||
// dealing with a reference
|
// dealing with a reference
|
||||||
if (!\is_object($data) || !$config->getByReference() || $form->getData() !== $this->propertyAccessor->getValue($data, $propertyPath)) {
|
if (!\is_object($data) || !$config->getByReference() || $propertyValue !== $this->propertyAccessor->getValue($data, $propertyPath)) {
|
||||||
$this->propertyAccessor->setValue($data, $propertyPath, $form->getData());
|
$this->propertyAccessor->setValue($data, $propertyPath, $propertyValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Form\Extension\Core\EventListener;
|
||||||
|
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Form\FormError;
|
||||||
|
use Symfony\Component\Form\FormEvent;
|
||||||
|
use Symfony\Component\Form\FormEvents;
|
||||||
|
use Symfony\Component\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
|
||||||
|
*/
|
||||||
|
class TransformationFailureListener implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
private $translator;
|
||||||
|
|
||||||
|
public function __construct(TranslatorInterface $translator = null)
|
||||||
|
{
|
||||||
|
$this->translator = $translator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
FormEvents::POST_SUBMIT => array('convertTransformationFailureToFormError', -1024),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function convertTransformationFailureToFormError(FormEvent $event)
|
||||||
|
{
|
||||||
|
$form = $event->getForm();
|
||||||
|
|
||||||
|
if (null === $form->getTransformationFailure() || !$form->isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($form as $child) {
|
||||||
|
if (!$child->isSynchronized()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientDataAsString = is_scalar($form->getViewData()) ? (string) $form->getViewData() : \gettype($form->getViewData());
|
||||||
|
$messageTemplate = 'The value {{ value }} is not valid.';
|
||||||
|
|
||||||
|
if (null !== $this->translator) {
|
||||||
|
$message = $this->translator->trans($messageTemplate, array('{{ value }}' => $clientDataAsString));
|
||||||
|
} else {
|
||||||
|
$message = strtr($messageTemplate, array('{{ value }}' => $clientDataAsString));
|
||||||
|
}
|
||||||
|
|
||||||
|
$form->addError(new FormError($message, $messageTemplate, array('{{ value }}' => $clientDataAsString), null, $form->getTransformationFailure()));
|
||||||
|
}
|
||||||
|
}
|
@ -91,6 +91,9 @@ class DateTimeType extends AbstractType
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// when the form is compound the entries of the array are ignored in favor of children data
|
||||||
|
// so we need to handle the cascade setting here
|
||||||
|
$emptyData = $builder->getEmptyData() ?: array();
|
||||||
// Only pass a subset of the options to children
|
// Only pass a subset of the options to children
|
||||||
$dateOptions = array_intersect_key($options, array_flip(array(
|
$dateOptions = array_intersect_key($options, array_flip(array(
|
||||||
'years',
|
'years',
|
||||||
@ -105,6 +108,10 @@ class DateTimeType extends AbstractType
|
|||||||
'invalid_message_parameters',
|
'invalid_message_parameters',
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
if (isset($emptyData['date'])) {
|
||||||
|
$dateOptions['empty_data'] = $emptyData['date'];
|
||||||
|
}
|
||||||
|
|
||||||
$timeOptions = array_intersect_key($options, array_flip(array(
|
$timeOptions = array_intersect_key($options, array_flip(array(
|
||||||
'hours',
|
'hours',
|
||||||
'minutes',
|
'minutes',
|
||||||
@ -120,6 +127,10 @@ class DateTimeType extends AbstractType
|
|||||||
'invalid_message_parameters',
|
'invalid_message_parameters',
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
if (isset($emptyData['time'])) {
|
||||||
|
$timeOptions['empty_data'] = $emptyData['time'];
|
||||||
|
}
|
||||||
|
|
||||||
if (false === $options['label']) {
|
if (false === $options['label']) {
|
||||||
$dateOptions['label'] = false;
|
$dateOptions['label'] = false;
|
||||||
$timeOptions['label'] = false;
|
$timeOptions['label'] = false;
|
||||||
@ -237,6 +248,9 @@ class DateTimeType extends AbstractType
|
|||||||
'compound' => $compound,
|
'compound' => $compound,
|
||||||
'date_label' => null,
|
'date_label' => null,
|
||||||
'time_label' => null,
|
'time_label' => null,
|
||||||
|
'empty_data' => function (Options $options) {
|
||||||
|
return $options['compound'] ? array() : '';
|
||||||
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
// Don't add some defaults in order to preserve the defaults
|
// Don't add some defaults in order to preserve the defaults
|
||||||
|
@ -76,7 +76,21 @@ class DateType extends AbstractType
|
|||||||
|
|
||||||
$yearOptions = $monthOptions = $dayOptions = array(
|
$yearOptions = $monthOptions = $dayOptions = array(
|
||||||
'error_bubbling' => true,
|
'error_bubbling' => true,
|
||||||
|
'empty_data' => '',
|
||||||
);
|
);
|
||||||
|
// when the form is compound the entries of the array are ignored in favor of children data
|
||||||
|
// so we need to handle the cascade setting here
|
||||||
|
$emptyData = $builder->getEmptyData() ?: array();
|
||||||
|
|
||||||
|
if (isset($emptyData['year'])) {
|
||||||
|
$yearOptions['empty_data'] = $emptyData['year'];
|
||||||
|
}
|
||||||
|
if (isset($emptyData['month'])) {
|
||||||
|
$monthOptions['empty_data'] = $emptyData['month'];
|
||||||
|
}
|
||||||
|
if (isset($emptyData['day'])) {
|
||||||
|
$dayOptions['empty_data'] = $emptyData['day'];
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($options['invalid_message'])) {
|
if (isset($options['invalid_message'])) {
|
||||||
$dayOptions['invalid_message'] = $options['invalid_message'];
|
$dayOptions['invalid_message'] = $options['invalid_message'];
|
||||||
@ -265,6 +279,9 @@ class DateType extends AbstractType
|
|||||||
// this option.
|
// this option.
|
||||||
'data_class' => null,
|
'data_class' => null,
|
||||||
'compound' => $compound,
|
'compound' => $compound,
|
||||||
|
'empty_data' => function (Options $options) {
|
||||||
|
return $options['compound'] ? array() : '';
|
||||||
|
},
|
||||||
'choice_translation_domain' => false,
|
'choice_translation_domain' => false,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -71,7 +71,15 @@ class TimeType extends AbstractType
|
|||||||
} else {
|
} else {
|
||||||
$hourOptions = $minuteOptions = $secondOptions = array(
|
$hourOptions = $minuteOptions = $secondOptions = array(
|
||||||
'error_bubbling' => true,
|
'error_bubbling' => true,
|
||||||
|
'empty_data' => '',
|
||||||
);
|
);
|
||||||
|
// when the form is compound the entries of the array are ignored in favor of children data
|
||||||
|
// so we need to handle the cascade setting here
|
||||||
|
$emptyData = $builder->getEmptyData() ?: array();
|
||||||
|
|
||||||
|
if (isset($emptyData['hour'])) {
|
||||||
|
$hourOptions['empty_data'] = $emptyData['hour'];
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($options['invalid_message'])) {
|
if (isset($options['invalid_message'])) {
|
||||||
$hourOptions['invalid_message'] = $options['invalid_message'];
|
$hourOptions['invalid_message'] = $options['invalid_message'];
|
||||||
@ -136,10 +144,16 @@ class TimeType extends AbstractType
|
|||||||
$builder->add('hour', self::$widgets[$options['widget']], $hourOptions);
|
$builder->add('hour', self::$widgets[$options['widget']], $hourOptions);
|
||||||
|
|
||||||
if ($options['with_minutes']) {
|
if ($options['with_minutes']) {
|
||||||
|
if (isset($emptyData['minute'])) {
|
||||||
|
$minuteOptions['empty_data'] = $emptyData['minute'];
|
||||||
|
}
|
||||||
$builder->add('minute', self::$widgets[$options['widget']], $minuteOptions);
|
$builder->add('minute', self::$widgets[$options['widget']], $minuteOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($options['with_seconds']) {
|
if ($options['with_seconds']) {
|
||||||
|
if (isset($emptyData['second'])) {
|
||||||
|
$secondOptions['empty_data'] = $emptyData['second'];
|
||||||
|
}
|
||||||
$builder->add('second', self::$widgets[$options['widget']], $secondOptions);
|
$builder->add('second', self::$widgets[$options['widget']], $secondOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,6 +272,9 @@ class TimeType extends AbstractType
|
|||||||
// representation is not \DateTime, but an array, we need to unset
|
// representation is not \DateTime, but an array, we need to unset
|
||||||
// this option.
|
// this option.
|
||||||
'data_class' => null,
|
'data_class' => null,
|
||||||
|
'empty_data' => function (Options $options) {
|
||||||
|
return $options['compound'] ? array() : '';
|
||||||
|
},
|
||||||
'compound' => $compound,
|
'compound' => $compound,
|
||||||
'choice_translation_domain' => false,
|
'choice_translation_domain' => false,
|
||||||
));
|
));
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Form\Extension\Core\Type;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractTypeExtension;
|
||||||
|
use Symfony\Component\Form\Extension\Core\EventListener\TransformationFailureListener;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
|
||||||
|
*/
|
||||||
|
class TransformationFailureExtension extends AbstractTypeExtension
|
||||||
|
{
|
||||||
|
private $translator;
|
||||||
|
|
||||||
|
public function __construct(TranslatorInterface $translator = null)
|
||||||
|
{
|
||||||
|
$this->translator = $translator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
if (!isset($options['invalid_message']) && !isset($options['invalid_message_parameters'])) {
|
||||||
|
$builder->addEventSubscriber(new TransformationFailureListener($this->translator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExtendedType()
|
||||||
|
{
|
||||||
|
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
|
||||||
|
}
|
||||||
|
}
|
@ -31,16 +31,18 @@ use Symfony\Component\PropertyAccess\PropertyPath;
|
|||||||
*
|
*
|
||||||
* (1) the "model" format required by the form's object
|
* (1) the "model" format required by the form's object
|
||||||
* (2) the "normalized" format for internal processing
|
* (2) the "normalized" format for internal processing
|
||||||
* (3) the "view" format used for display
|
* (3) the "view" format used for display simple fields
|
||||||
|
* or map children model data for compound fields
|
||||||
*
|
*
|
||||||
* A date field, for example, may store a date as "Y-m-d" string (1) in the
|
* A date field, for example, may store a date as "Y-m-d" string (1) in the
|
||||||
* object. To facilitate processing in the field, this value is normalized
|
* object. To facilitate processing in the field, this value is normalized
|
||||||
* to a DateTime object (2). In the HTML representation of your form, a
|
* to a DateTime object (2). In the HTML representation of your form, a
|
||||||
* localized string (3) is presented to and modified by the user.
|
* localized string (3) may be presented to and modified by the user, or it could be an array of values
|
||||||
|
* to be mapped to choices fields.
|
||||||
*
|
*
|
||||||
* In most cases, format (1) and format (2) will be the same. For example,
|
* In most cases, format (1) and format (2) will be the same. For example,
|
||||||
* a checkbox field uses a Boolean value for both internal processing and
|
* a checkbox field uses a Boolean value for both internal processing and
|
||||||
* storage in the object. In these cases you simply need to set a value
|
* storage in the object. In these cases you simply need to set a view
|
||||||
* transformer to convert between formats (2) and (3). You can do this by
|
* transformer to convert between formats (2) and (3). You can do this by
|
||||||
* calling addViewTransformer().
|
* calling addViewTransformer().
|
||||||
*
|
*
|
||||||
@ -48,7 +50,7 @@ use Symfony\Component\PropertyAccess\PropertyPath;
|
|||||||
* demonstrate this, let's extend our above date field to store the value
|
* demonstrate this, let's extend our above date field to store the value
|
||||||
* either as "Y-m-d" string or as timestamp. Internally we still want to
|
* either as "Y-m-d" string or as timestamp. Internally we still want to
|
||||||
* use a DateTime object for processing. To convert the data from string/integer
|
* use a DateTime object for processing. To convert the data from string/integer
|
||||||
* to DateTime you can set a normalization transformer by calling
|
* to DateTime you can set a model transformer by calling
|
||||||
* addModelTransformer(). The normalized data is then converted to the displayed
|
* addModelTransformer(). The normalized data is then converted to the displayed
|
||||||
* data as described before.
|
* data as described before.
|
||||||
*
|
*
|
||||||
@ -217,7 +219,7 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (null === $this->getName() || '' === $this->getName()) {
|
if (null === $this->getName() || '' === $this->getName()) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$parent = $this->parent;
|
$parent = $this->parent;
|
||||||
@ -340,8 +342,8 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
|
|||||||
$modelData = $event->getData();
|
$modelData = $event->getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Treat data as strings unless a value transformer exists
|
// Treat data as strings unless a transformer exists
|
||||||
if (!$this->config->getViewTransformers() && !$this->config->getModelTransformers() && is_scalar($modelData)) {
|
if (is_scalar($modelData) && !$this->config->getViewTransformers() && !$this->config->getModelTransformers()) {
|
||||||
$modelData = (string) $modelData;
|
$modelData = (string) $modelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1035,7 +1037,7 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes the value if a normalization transformer is set.
|
* Normalizes the value if a model transformer is set.
|
||||||
*
|
*
|
||||||
* @param mixed $value The value to transform
|
* @param mixed $value The value to transform
|
||||||
*
|
*
|
||||||
@ -1057,7 +1059,7 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reverse transforms a value if a normalization transformer is set.
|
* Reverse transforms a value if a model transformer is set.
|
||||||
*
|
*
|
||||||
* @param string $value The value to reverse transform
|
* @param string $value The value to reverse transform
|
||||||
*
|
*
|
||||||
@ -1081,7 +1083,7 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms the value if a value transformer is set.
|
* Transforms the value if a view transformer is set.
|
||||||
*
|
*
|
||||||
* @param mixed $value The value to transform
|
* @param mixed $value The value to transform
|
||||||
*
|
*
|
||||||
@ -1112,7 +1114,7 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reverse transforms a value if a value transformer is set.
|
* Reverse transforms a value if a view transformer is set.
|
||||||
*
|
*
|
||||||
* @param string $value The value to reverse transform
|
* @param string $value The value to reverse transform
|
||||||
*
|
*
|
||||||
|
@ -15,7 +15,7 @@ use Symfony\Component\Form\Exception\UnexpectedTypeException;
|
|||||||
use Symfony\Component\Form\Util\ServerParams;
|
use Symfony\Component\Form\Util\ServerParams;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A request handler using PHP's super globals $_GET, $_POST and $_SERVER.
|
* A request handler using PHP super globals $_GET, $_POST and $_SERVER.
|
||||||
*
|
*
|
||||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||||
*/
|
*/
|
||||||
@ -213,7 +213,7 @@ class NativeRequestHandler implements RequestHandlerInterface
|
|||||||
|
|
||||||
if (self::$fileKeys === $keys) {
|
if (self::$fileKeys === $keys) {
|
||||||
if (UPLOAD_ERR_NO_FILE === $data['error']) {
|
if (UPLOAD_ERR_NO_FILE === $data['error']) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Form\Tests\Extension\Core;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Form\Extension\Core\CoreExtension;
|
||||||
|
use Symfony\Component\Form\FormFactoryBuilder;
|
||||||
|
|
||||||
|
class CoreExtensionTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testTransformationFailuresAreConvertedIntoFormErrors()
|
||||||
|
{
|
||||||
|
$formFactoryBuilder = new FormFactoryBuilder();
|
||||||
|
$formFactory = $formFactoryBuilder->addExtension(new CoreExtension())
|
||||||
|
->getFormFactory();
|
||||||
|
|
||||||
|
$form = $formFactory->createBuilder()
|
||||||
|
->add('foo', 'Symfony\Component\Form\Extension\Core\Type\DateType')
|
||||||
|
->getForm();
|
||||||
|
$form->submit('foo');
|
||||||
|
|
||||||
|
$this->assertFalse($form->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -353,4 +353,39 @@ class PropertyPathMapperTest extends TestCase
|
|||||||
|
|
||||||
$this->mapper->mapFormsToData(array($form), $car);
|
$this->mapper->mapFormsToData(array($form), $car);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideDate
|
||||||
|
*/
|
||||||
|
public function testMapFormsToDataDoesNotChangeEqualDateTimeInstance($date)
|
||||||
|
{
|
||||||
|
$article = array();
|
||||||
|
$publishedAt = $date;
|
||||||
|
$article['publishedAt'] = clone $publishedAt;
|
||||||
|
$propertyPath = $this->getPropertyPath('[publishedAt]');
|
||||||
|
|
||||||
|
$this->propertyAccessor->expects($this->once())
|
||||||
|
->method('getValue')
|
||||||
|
->willReturn($article['publishedAt'])
|
||||||
|
;
|
||||||
|
$this->propertyAccessor->expects($this->never())
|
||||||
|
->method('setValue')
|
||||||
|
;
|
||||||
|
|
||||||
|
$config = new FormConfigBuilder('publishedAt', \get_class($publishedAt), $this->dispatcher);
|
||||||
|
$config->setByReference(false);
|
||||||
|
$config->setPropertyPath($propertyPath);
|
||||||
|
$config->setData($publishedAt);
|
||||||
|
$form = $this->getForm($config);
|
||||||
|
|
||||||
|
$this->mapper->mapFormsToData(array($form), $article);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideDate()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
array(new \DateTime()),
|
||||||
|
array(new \DateTimeImmutable()),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -644,4 +644,31 @@ class DateTimeTypeTest extends BaseTypeTest
|
|||||||
$this->assertSame($expectedData, $form->getNormData());
|
$this->assertSame($expectedData, $form->getNormData());
|
||||||
$this->assertSame($expectedData, $form->getData());
|
$this->assertSame($expectedData, $form->getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideEmptyData
|
||||||
|
*/
|
||||||
|
public function testSubmitNullUsesDateEmptyData($widget, $emptyData, $expectedData)
|
||||||
|
{
|
||||||
|
$form = $this->factory->create(static::TESTED_TYPE, null, array(
|
||||||
|
'widget' => $widget,
|
||||||
|
'empty_data' => $emptyData,
|
||||||
|
));
|
||||||
|
$form->submit(null);
|
||||||
|
|
||||||
|
$this->assertSame($emptyData, $form->getViewData());
|
||||||
|
$this->assertEquals($expectedData, $form->getNormData());
|
||||||
|
$this->assertEquals($expectedData, $form->getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideEmptyData()
|
||||||
|
{
|
||||||
|
$expectedData = \DateTime::createFromFormat('Y-m-d H:i', '2018-11-11 21:23');
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'Simple field' => array('single_text', '2018-11-11T21:23:00', $expectedData),
|
||||||
|
'Compound text field' => array('text', array('date' => array('year' => '2018', 'month' => '11', 'day' => '11'), 'time' => array('hour' => '21', 'minute' => '23')), $expectedData),
|
||||||
|
'Compound choice field' => array('choice', array('date' => array('year' => '2018', 'month' => '11', 'day' => '11'), 'time' => array('hour' => '21', 'minute' => '23')), $expectedData),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1005,25 +1005,36 @@ class DateTypeTest extends BaseTypeTest
|
|||||||
));
|
));
|
||||||
$form->submit(null);
|
$form->submit(null);
|
||||||
|
|
||||||
// view transformer write back empty strings in the view data
|
// view transformer writes back empty strings in the view data
|
||||||
$this->assertSame(array('year' => '', 'month' => '', 'day' => ''), $form->getViewData());
|
$this->assertSame(array('year' => '', 'month' => '', 'day' => ''), $form->getViewData());
|
||||||
$this->assertSame($expectedData, $form->getNormData());
|
$this->assertSame($expectedData, $form->getNormData());
|
||||||
$this->assertSame($expectedData, $form->getData());
|
$this->assertSame($expectedData, $form->getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSingleTextSubmitNullUsesDefaultEmptyData()
|
/**
|
||||||
|
* @dataProvider provideEmptyData
|
||||||
|
*/
|
||||||
|
public function testSubmitNullUsesDateEmptyData($widget, $emptyData, $expectedData)
|
||||||
{
|
{
|
||||||
$emptyData = '2018-11-11';
|
|
||||||
$form = $this->factory->create(static::TESTED_TYPE, null, array(
|
$form = $this->factory->create(static::TESTED_TYPE, null, array(
|
||||||
'widget' => 'single_text',
|
'widget' => $widget,
|
||||||
'empty_data' => $emptyData,
|
'empty_data' => $emptyData,
|
||||||
));
|
));
|
||||||
$form->submit(null);
|
$form->submit(null);
|
||||||
|
|
||||||
$date = new \DateTime($emptyData);
|
|
||||||
|
|
||||||
$this->assertSame($emptyData, $form->getViewData());
|
$this->assertSame($emptyData, $form->getViewData());
|
||||||
$this->assertEquals($date, $form->getNormData());
|
$this->assertEquals($expectedData, $form->getNormData());
|
||||||
$this->assertEquals($date, $form->getData());
|
$this->assertEquals($expectedData, $form->getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideEmptyData()
|
||||||
|
{
|
||||||
|
$expectedData = \DateTime::createFromFormat('Y-m-d H:i:s', '2018-11-11 00:00:00');
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'Simple field' => array('single_text', '2018-11-11', $expectedData),
|
||||||
|
'Compound text fields' => array('text', array('year' => '2018', 'month' => '11', 'day' => '11'), $expectedData),
|
||||||
|
'Compound choice fields' => array('choice', array('year' => '2018', 'month' => '11', 'day' => '11'), $expectedData),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -798,9 +798,36 @@ class TimeTypeTest extends BaseTypeTest
|
|||||||
));
|
));
|
||||||
$form->submit(null);
|
$form->submit(null);
|
||||||
|
|
||||||
// view transformer write back empty strings in the view data
|
// view transformer writes back empty strings in the view data
|
||||||
$this->assertSame(array('hour' => '', 'minute' => ''), $form->getViewData());
|
$this->assertSame(array('hour' => '', 'minute' => ''), $form->getViewData());
|
||||||
$this->assertSame($expectedData, $form->getNormData());
|
$this->assertSame($expectedData, $form->getNormData());
|
||||||
$this->assertSame($expectedData, $form->getData());
|
$this->assertSame($expectedData, $form->getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideEmptyData
|
||||||
|
*/
|
||||||
|
public function testSubmitNullUsesDateEmptyData($widget, $emptyData, $expectedData)
|
||||||
|
{
|
||||||
|
$form = $this->factory->create(static::TESTED_TYPE, null, array(
|
||||||
|
'widget' => $widget,
|
||||||
|
'empty_data' => $emptyData,
|
||||||
|
));
|
||||||
|
$form->submit(null);
|
||||||
|
|
||||||
|
$this->assertSame($emptyData, $form->getViewData());
|
||||||
|
$this->assertEquals($expectedData, $form->getNormData());
|
||||||
|
$this->assertEquals($expectedData, $form->getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideEmptyData()
|
||||||
|
{
|
||||||
|
$expectedData = \DateTime::createFromFormat('Y-m-d H:i', '1970-01-01 21:23');
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'Simple field' => array('single_text', '21:23', $expectedData),
|
||||||
|
'Compound text field' => array('text', array('hour' => '21', 'minute' => '23'), $expectedData),
|
||||||
|
'Compound choice field' => array('choice', array('hour' => '21', 'minute' => '23'), $expectedData),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ class FormUtil
|
|||||||
* Returns whether the given data is empty.
|
* Returns whether the given data is empty.
|
||||||
*
|
*
|
||||||
* This logic is reused multiple times throughout the processing of
|
* This logic is reused multiple times throughout the processing of
|
||||||
* a form and needs to be consistent. PHP's keyword `empty` cannot
|
* a form and needs to be consistent. PHP keyword `empty` cannot
|
||||||
* be used as it also considers 0 and "0" to be empty.
|
* be used as it also considers 0 and "0" to be empty.
|
||||||
*
|
*
|
||||||
* @param mixed $data
|
* @param mixed $data
|
||||||
|
@ -128,7 +128,7 @@ class OrderedHashMap implements \ArrayAccess, \IteratorAggregate, \Countable
|
|||||||
$key = array() === $this->orderedKeys
|
$key = array() === $this->orderedKeys
|
||||||
// If the array is empty, use 0 as key
|
// If the array is empty, use 0 as key
|
||||||
? 0
|
? 0
|
||||||
// Imitate PHP's behavior of generating a key that equals
|
// Imitate PHP behavior of generating a key that equals
|
||||||
// the highest existing integer key + 1
|
// the highest existing integer key + 1
|
||||||
: 1 + (int) max($this->orderedKeys);
|
: 1 + (int) max($this->orderedKeys);
|
||||||
}
|
}
|
||||||
|
@ -56,8 +56,6 @@ class OrderedHashMapIterator implements \Iterator
|
|||||||
private $current;
|
private $current;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new iterator.
|
|
||||||
*
|
|
||||||
* @param array $elements The elements of the map, indexed by their
|
* @param array $elements The elements of the map, indexed by their
|
||||||
* keys
|
* keys
|
||||||
* @param array $orderedKeys The keys of the map in the order in which
|
* @param array $orderedKeys The keys of the map in the order in which
|
||||||
@ -84,7 +82,7 @@ class OrderedHashMapIterator implements \Iterator
|
|||||||
*/
|
*/
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
{
|
{
|
||||||
// Use array_splice() instead of isset() to prevent holes in the
|
// Use array_splice() instead of unset() to prevent holes in the
|
||||||
// array indices, which would break the initialization of $cursorId
|
// array indices, which would break the initialization of $cursorId
|
||||||
array_splice($this->managedCursors, $this->cursorId, 1);
|
array_splice($this->managedCursors, $this->cursorId, 1);
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ CHANGELOG
|
|||||||
|
|
||||||
* added orphaned events support to `EventDataCollector`
|
* added orphaned events support to `EventDataCollector`
|
||||||
* `ExceptionListener` now logs exceptions at priority `0` (previously logged at `-128`)
|
* `ExceptionListener` now logs exceptions at priority `0` (previously logged at `-128`)
|
||||||
* Deprecated `service:action` syntax with a single colon to reference controllers. Use `service::method` instead.
|
* Added support for using `service::method` to reference controllers, making it consistent with other cases. It is recommended over the `service:action` syntax with a single colon, which will be deprecated in the future.
|
||||||
* Added the ability to profile individual argument value resolvers via the
|
* Added the ability to profile individual argument value resolvers via the
|
||||||
`Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver`
|
`Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver`
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class ContainerControllerResolver extends ControllerResolver
|
|||||||
{
|
{
|
||||||
if (1 === substr_count($controller, ':')) {
|
if (1 === substr_count($controller, ':')) {
|
||||||
$controller = str_replace(':', '::', $controller);
|
$controller = str_replace(':', '::', $controller);
|
||||||
@trigger_error(sprintf('Referencing controllers with a single colon is deprecated since Symfony 4.1. Use %s instead.', $controller), E_USER_DEPRECATED);
|
// TODO deprecate this in 5.1
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::createController($controller);
|
return parent::createController($controller);
|
||||||
|
@ -19,10 +19,6 @@ use Symfony\Component\HttpKernel\Controller\ContainerControllerResolver;
|
|||||||
|
|
||||||
class ContainerControllerResolverTest extends ControllerResolverTest
|
class ContainerControllerResolverTest extends ControllerResolverTest
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @group legacy
|
|
||||||
* @expectedDeprecation Referencing controllers with a single colon is deprecated since Symfony 4.1. Use foo::action instead.
|
|
||||||
*/
|
|
||||||
public function testGetControllerServiceWithSingleColon()
|
public function testGetControllerServiceWithSingleColon()
|
||||||
{
|
{
|
||||||
$service = new ControllerTestService('foo');
|
$service = new ControllerTestService('foo');
|
||||||
|
@ -607,16 +607,6 @@ class PropertyAccessor implements PropertyAccessorInterface
|
|||||||
$camelized = $this->camelize($property);
|
$camelized = $this->camelize($property);
|
||||||
$singulars = (array) Inflector::singularize($camelized);
|
$singulars = (array) Inflector::singularize($camelized);
|
||||||
|
|
||||||
if (\is_array($value) || $value instanceof \Traversable) {
|
|
||||||
$methods = $this->findAdderAndRemover($reflClass, $singulars);
|
|
||||||
|
|
||||||
if (null !== $methods) {
|
|
||||||
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
|
|
||||||
$access[self::ACCESS_ADDER] = $methods[0];
|
|
||||||
$access[self::ACCESS_REMOVER] = $methods[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($access[self::ACCESS_TYPE])) {
|
if (!isset($access[self::ACCESS_TYPE])) {
|
||||||
$setter = 'set'.$camelized;
|
$setter = 'set'.$camelized;
|
||||||
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
|
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
|
||||||
@ -638,16 +628,22 @@ class PropertyAccessor implements PropertyAccessorInterface
|
|||||||
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
|
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
|
||||||
$access[self::ACCESS_NAME] = $setter;
|
$access[self::ACCESS_NAME] = $setter;
|
||||||
} elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars)) {
|
} elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars)) {
|
||||||
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
|
if (\is_array($value) || $value instanceof \Traversable) {
|
||||||
$access[self::ACCESS_NAME] = sprintf(
|
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
|
||||||
'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
|
$access[self::ACCESS_ADDER] = $methods[0];
|
||||||
'the new value must be an array or an instance of \Traversable, '.
|
$access[self::ACCESS_REMOVER] = $methods[1];
|
||||||
'"%s" given.',
|
} else {
|
||||||
$property,
|
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
|
||||||
$reflClass->name,
|
$access[self::ACCESS_NAME] = sprintf(
|
||||||
implode('()", "', $methods),
|
'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
|
||||||
\is_object($value) ? \get_class($value) : \gettype($value)
|
'the new value must be an array or an instance of \Traversable, '.
|
||||||
);
|
'"%s" given.',
|
||||||
|
$property,
|
||||||
|
$reflClass->name,
|
||||||
|
implode('()", "', $methods),
|
||||||
|
\is_object($value) ? \get_class($value) : \gettype($value)
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
|
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
|
||||||
$access[self::ACCESS_NAME] = sprintf(
|
$access[self::ACCESS_NAME] = sprintf(
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notice we don't have getter/setter for emails
|
||||||
|
* because we count on adder/remover.
|
||||||
|
*/
|
||||||
|
class TestSingularAndPluralProps
|
||||||
|
{
|
||||||
|
/** @var string|null */
|
||||||
|
private $email;
|
||||||
|
|
||||||
|
/** @var array */
|
||||||
|
private $emails = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getEmail()
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|null $email
|
||||||
|
*/
|
||||||
|
public function setEmail($email)
|
||||||
|
{
|
||||||
|
$this->email = $email;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getEmails()
|
||||||
|
{
|
||||||
|
return $this->emails;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $email
|
||||||
|
*/
|
||||||
|
public function addEmail($email)
|
||||||
|
{
|
||||||
|
$this->emails[] = $email;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $email
|
||||||
|
*/
|
||||||
|
public function removeEmail($email)
|
||||||
|
{
|
||||||
|
$this->emails = array_diff($this->emails, array($email));
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall;
|
|||||||
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet;
|
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet;
|
||||||
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassSetValue;
|
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassSetValue;
|
||||||
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassTypeErrorInsideCall;
|
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassTypeErrorInsideCall;
|
||||||
|
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestSingularAndPluralProps;
|
||||||
use Symfony\Component\PropertyAccess\Tests\Fixtures\Ticket5775Object;
|
use Symfony\Component\PropertyAccess\Tests\Fixtures\Ticket5775Object;
|
||||||
use Symfony\Component\PropertyAccess\Tests\Fixtures\TypeHinted;
|
use Symfony\Component\PropertyAccess\Tests\Fixtures\TypeHinted;
|
||||||
|
|
||||||
@ -675,4 +676,26 @@ class PropertyAccessorTest extends TestCase
|
|||||||
|
|
||||||
$this->propertyAccessor->setValue($object, 'name', 'foo');
|
$this->propertyAccessor->setValue($object, 'name', 'foo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testWriteToSingularPropertyWhilePluralOneExists()
|
||||||
|
{
|
||||||
|
$object = new TestSingularAndPluralProps();
|
||||||
|
|
||||||
|
$this->propertyAccessor->isWritable($object, 'email'); //cache access info
|
||||||
|
$this->propertyAccessor->setValue($object, 'email', 'test@email.com');
|
||||||
|
|
||||||
|
self::assertEquals('test@email.com', $object->getEmail());
|
||||||
|
self::assertEmpty($object->getEmails());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWriteToPluralPropertyWhileSingularOneExists()
|
||||||
|
{
|
||||||
|
$object = new TestSingularAndPluralProps();
|
||||||
|
|
||||||
|
$this->propertyAccessor->isWritable($object, 'emails'); //cache access info
|
||||||
|
$this->propertyAccessor->setValue($object, 'emails', array('test@email.com'));
|
||||||
|
|
||||||
|
self::assertEquals(array('test@email.com'), $object->getEmails());
|
||||||
|
self::assertNull($object->getEmail());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ EOF;
|
|||||||
?? $this->context->getParameter('_locale')
|
?? $this->context->getParameter('_locale')
|
||||||
?: $this->defaultLocale;
|
?: $this->defaultLocale;
|
||||||
|
|
||||||
if (null !== $locale) {
|
if (null !== $locale && null !== $name) {
|
||||||
do {
|
do {
|
||||||
if ((self::$declaredRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) {
|
if ((self::$declaredRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) {
|
||||||
unset($parameters['_locale']);
|
unset($parameters['_locale']);
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Workflow\EventListener;
|
||||||
|
|
||||||
|
use Symfony\Component\Workflow\Transition;
|
||||||
|
|
||||||
|
class GuardExpression
|
||||||
|
{
|
||||||
|
private $transition;
|
||||||
|
|
||||||
|
private $expression;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $expression
|
||||||
|
*/
|
||||||
|
public function __construct(Transition $transition, $expression)
|
||||||
|
{
|
||||||
|
$this->transition = $transition;
|
||||||
|
$this->expression = $expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTransition()
|
||||||
|
{
|
||||||
|
return $this->transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpression()
|
||||||
|
{
|
||||||
|
return $this->expression;
|
||||||
|
}
|
||||||
|
}
|
@ -50,8 +50,21 @@ class GuardListener
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$expression = $this->configuration[$eventName];
|
$eventConfiguration = (array) $this->configuration[$eventName];
|
||||||
|
foreach ($eventConfiguration as $guard) {
|
||||||
|
if ($guard instanceof GuardExpression) {
|
||||||
|
if ($guard->getTransition() !== $event->getTransition()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->validateGuardExpression($event, $guard->getExpression());
|
||||||
|
} else {
|
||||||
|
$this->validateGuardExpression($event, $guard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateGuardExpression(GuardEvent $event, string $expression)
|
||||||
|
{
|
||||||
if (!$this->expressionLanguage->evaluate($expression, $this->getVariables($event))) {
|
if (!$this->expressionLanguage->evaluate($expression, $this->getVariables($event))) {
|
||||||
$blocker = TransitionBlocker::createBlockedByExpressionGuardListener($expression);
|
$blocker = TransitionBlocker::createBlockedByExpressionGuardListener($expression);
|
||||||
$event->addTransitionBlocker($blocker);
|
$event->addTransitionBlocker($blocker);
|
||||||
|
@ -11,6 +11,7 @@ use Symfony\Component\Security\Core\Role\Role;
|
|||||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||||
use Symfony\Component\Workflow\EventListener\ExpressionLanguage;
|
use Symfony\Component\Workflow\EventListener\ExpressionLanguage;
|
||||||
|
use Symfony\Component\Workflow\EventListener\GuardExpression;
|
||||||
use Symfony\Component\Workflow\EventListener\GuardListener;
|
use Symfony\Component\Workflow\EventListener\GuardListener;
|
||||||
use Symfony\Component\Workflow\Marking;
|
use Symfony\Component\Workflow\Marking;
|
||||||
use Symfony\Component\Workflow\Transition;
|
use Symfony\Component\Workflow\Transition;
|
||||||
@ -21,12 +22,17 @@ class GuardListenerTest extends TestCase
|
|||||||
private $authenticationChecker;
|
private $authenticationChecker;
|
||||||
private $validator;
|
private $validator;
|
||||||
private $listener;
|
private $listener;
|
||||||
|
private $configuration;
|
||||||
|
|
||||||
protected function setUp()
|
protected function setUp()
|
||||||
{
|
{
|
||||||
$configuration = array(
|
$this->configuration = array(
|
||||||
'test_is_granted' => 'is_granted("something")',
|
'test_is_granted' => 'is_granted("something")',
|
||||||
'test_is_valid' => 'is_valid(subject)',
|
'test_is_valid' => 'is_valid(subject)',
|
||||||
|
'test_expression' => array(
|
||||||
|
new GuardExpression(new Transition('name', 'from', 'to'), '!is_valid(subject)'),
|
||||||
|
new GuardExpression(new Transition('name', 'from', 'to'), 'is_valid(subject)'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
$expressionLanguage = new ExpressionLanguage();
|
$expressionLanguage = new ExpressionLanguage();
|
||||||
$token = $this->getMockBuilder(TokenInterface::class)->getMock();
|
$token = $this->getMockBuilder(TokenInterface::class)->getMock();
|
||||||
@ -36,7 +42,7 @@ class GuardListenerTest extends TestCase
|
|||||||
$this->authenticationChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
|
$this->authenticationChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
|
||||||
$trustResolver = $this->getMockBuilder(AuthenticationTrustResolverInterface::class)->getMock();
|
$trustResolver = $this->getMockBuilder(AuthenticationTrustResolverInterface::class)->getMock();
|
||||||
$this->validator = $this->getMockBuilder(ValidatorInterface::class)->getMock();
|
$this->validator = $this->getMockBuilder(ValidatorInterface::class)->getMock();
|
||||||
$this->listener = new GuardListener($configuration, $expressionLanguage, $tokenStorage, $this->authenticationChecker, $trustResolver, null, $this->validator);
|
$this->listener = new GuardListener($this->configuration, $expressionLanguage, $tokenStorage, $this->authenticationChecker, $trustResolver, null, $this->validator);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown()
|
protected function tearDown()
|
||||||
@ -97,11 +103,38 @@ class GuardListenerTest extends TestCase
|
|||||||
$this->assertFalse($event->isBlocked());
|
$this->assertFalse($event->isBlocked());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createEvent()
|
public function testWithGuardExpressionWithNotSupportedTransition()
|
||||||
|
{
|
||||||
|
$event = $this->createEvent();
|
||||||
|
$this->configureValidator(false);
|
||||||
|
$this->listener->onTransition($event, 'test_expression');
|
||||||
|
|
||||||
|
$this->assertFalse($event->isBlocked());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithGuardExpressionWithSupportedTransition()
|
||||||
|
{
|
||||||
|
$event = $this->createEvent($this->configuration['test_expression'][1]->getTransition());
|
||||||
|
$this->configureValidator(true, true);
|
||||||
|
$this->listener->onTransition($event, 'test_expression');
|
||||||
|
|
||||||
|
$this->assertFalse($event->isBlocked());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGuardExpressionBlocks()
|
||||||
|
{
|
||||||
|
$event = $this->createEvent($this->configuration['test_expression'][1]->getTransition());
|
||||||
|
$this->configureValidator(true, false);
|
||||||
|
$this->listener->onTransition($event, 'test_expression');
|
||||||
|
|
||||||
|
$this->assertTrue($event->isBlocked());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createEvent(Transition $transition = null)
|
||||||
{
|
{
|
||||||
$subject = new \stdClass();
|
$subject = new \stdClass();
|
||||||
$subject->marking = new Marking();
|
$subject->marking = new Marking();
|
||||||
$transition = new Transition('name', 'from', 'to');
|
$transition = $transition ?: new Transition('name', 'from', 'to');
|
||||||
|
|
||||||
$workflow = $this->getMockBuilder(WorkflowInterface::class)->getMock();
|
$workflow = $this->getMockBuilder(WorkflowInterface::class)->getMock();
|
||||||
|
|
||||||
|
@ -3,7 +3,10 @@
|
|||||||
namespace Symfony\Component\Workflow\Tests;
|
namespace Symfony\Component\Workflow\Tests;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
|
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||||
use Symfony\Component\Workflow\StateMachine;
|
use Symfony\Component\Workflow\StateMachine;
|
||||||
|
use Symfony\Component\Workflow\TransitionBlocker;
|
||||||
|
|
||||||
class StateMachineTest extends TestCase
|
class StateMachineTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -38,4 +41,70 @@ class StateMachineTest extends TestCase
|
|||||||
$this->assertTrue($net->can($subject, 't2'));
|
$this->assertTrue($net->can($subject, 't2'));
|
||||||
$this->assertTrue($net->can($subject, 't3'));
|
$this->assertTrue($net->can($subject, 't3'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testBuildTransitionBlockerList()
|
||||||
|
{
|
||||||
|
$definition = $this->createComplexStateMachineDefinition();
|
||||||
|
|
||||||
|
$net = new StateMachine($definition);
|
||||||
|
$subject = new \stdClass();
|
||||||
|
|
||||||
|
$subject->marking = 'a';
|
||||||
|
$this->assertTrue($net->buildTransitionBlockerList($subject, 't1')->isEmpty());
|
||||||
|
$subject->marking = 'd';
|
||||||
|
$this->assertTrue($net->buildTransitionBlockerList($subject, 't1')->isEmpty());
|
||||||
|
|
||||||
|
$subject->marking = 'b';
|
||||||
|
$this->assertFalse($net->buildTransitionBlockerList($subject, 't1')->isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBuildTransitionBlockerListWithMultipleTransitions()
|
||||||
|
{
|
||||||
|
$definition = $this->createComplexStateMachineDefinition();
|
||||||
|
|
||||||
|
$net = new StateMachine($definition);
|
||||||
|
$subject = new \stdClass();
|
||||||
|
|
||||||
|
$subject->marking = 'b';
|
||||||
|
$this->assertTrue($net->buildTransitionBlockerList($subject, 't2')->isEmpty());
|
||||||
|
$this->assertTrue($net->buildTransitionBlockerList($subject, 't3')->isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBuildTransitionBlockerListReturnsExpectedReasonOnBranchMerge()
|
||||||
|
{
|
||||||
|
$definition = $this->createComplexStateMachineDefinition();
|
||||||
|
|
||||||
|
$dispatcher = new EventDispatcher();
|
||||||
|
$net = new StateMachine($definition, null, $dispatcher);
|
||||||
|
|
||||||
|
$dispatcher->addListener('workflow.guard', function (GuardEvent $event) {
|
||||||
|
$event->addTransitionBlocker(new TransitionBlocker(\sprintf('Transition blocker of place %s', $event->getTransition()->getFroms()[0]), 'blocker'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$subject = new \stdClass();
|
||||||
|
|
||||||
|
// There may be multiple transitions with the same name. Make sure that transitions
|
||||||
|
// that are not enabled by the marking are evaluated.
|
||||||
|
// see https://github.com/symfony/symfony/issues/28432
|
||||||
|
|
||||||
|
// Test if when you are in place "a"trying transition "t1" then returned
|
||||||
|
// blocker list contains guard blocker instead blockedByMarking
|
||||||
|
$subject->marking = 'a';
|
||||||
|
$transitionBlockerList = $net->buildTransitionBlockerList($subject, 't1');
|
||||||
|
$this->assertCount(1, $transitionBlockerList);
|
||||||
|
$blockers = iterator_to_array($transitionBlockerList);
|
||||||
|
|
||||||
|
$this->assertSame('Transition blocker of place a', $blockers[0]->getMessage());
|
||||||
|
$this->assertSame('blocker', $blockers[0]->getCode());
|
||||||
|
|
||||||
|
// Test if when you are in place "d" trying transition "t1" then
|
||||||
|
// returned blocker list contains guard blocker instead blockedByMarking
|
||||||
|
$subject->marking = 'd';
|
||||||
|
$transitionBlockerList = $net->buildTransitionBlockerList($subject, 't1');
|
||||||
|
$this->assertCount(1, $transitionBlockerList);
|
||||||
|
$blockers = iterator_to_array($transitionBlockerList);
|
||||||
|
|
||||||
|
$this->assertSame('Transition blocker of place d', $blockers[0]->getMessage());
|
||||||
|
$this->assertSame('blocker', $blockers[0]->getCode());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,6 +195,32 @@ class WorkflowTest extends TestCase
|
|||||||
$workflow->buildTransitionBlockerList($subject, '404 Not Found');
|
$workflow->buildTransitionBlockerList($subject, '404 Not Found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testBuildTransitionBlockerList()
|
||||||
|
{
|
||||||
|
$definition = $this->createComplexWorkflowDefinition();
|
||||||
|
$subject = new \stdClass();
|
||||||
|
$subject->marking = null;
|
||||||
|
$workflow = new Workflow($definition, new MultipleStateMarkingStore());
|
||||||
|
|
||||||
|
$this->assertTrue($workflow->buildTransitionBlockerList($subject, 't1')->isEmpty());
|
||||||
|
$this->assertFalse($workflow->buildTransitionBlockerList($subject, 't2')->isEmpty());
|
||||||
|
|
||||||
|
$subject->marking = array('b' => 1);
|
||||||
|
|
||||||
|
$this->assertFalse($workflow->buildTransitionBlockerList($subject, 't1')->isEmpty());
|
||||||
|
$this->assertFalse($workflow->buildTransitionBlockerList($subject, 't2')->isEmpty());
|
||||||
|
|
||||||
|
$subject->marking = array('b' => 1, 'c' => 1);
|
||||||
|
|
||||||
|
$this->assertFalse($workflow->buildTransitionBlockerList($subject, 't1')->isEmpty());
|
||||||
|
$this->assertTrue($workflow->buildTransitionBlockerList($subject, 't2')->isEmpty());
|
||||||
|
|
||||||
|
$subject->marking = array('f' => 1);
|
||||||
|
|
||||||
|
$this->assertFalse($workflow->buildTransitionBlockerList($subject, 't5')->isEmpty());
|
||||||
|
$this->assertTrue($workflow->buildTransitionBlockerList($subject, 't6')->isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
public function testBuildTransitionBlockerListReturnsReasonsProvidedByMarking()
|
public function testBuildTransitionBlockerListReturnsReasonsProvidedByMarking()
|
||||||
{
|
{
|
||||||
$definition = $this->createComplexWorkflowDefinition();
|
$definition = $this->createComplexWorkflowDefinition();
|
||||||
|
@ -37,6 +37,17 @@ final class TransitionBlockerList implements \IteratorAggregate, \Countable
|
|||||||
$this->blockers[] = $blocker;
|
$this->blockers[] = $blocker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function has(string $code): bool
|
||||||
|
{
|
||||||
|
foreach ($this->blockers as $blocker) {
|
||||||
|
if ($code === $blocker->getCode()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function clear(): void
|
public function clear(): void
|
||||||
{
|
{
|
||||||
$this->blockers = array();
|
$this->blockers = array();
|
||||||
|
@ -119,7 +119,15 @@ class Workflow implements WorkflowInterface
|
|||||||
$transitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);
|
$transitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);
|
||||||
|
|
||||||
if ($transitionBlockerList->isEmpty()) {
|
if ($transitionBlockerList->isEmpty()) {
|
||||||
continue;
|
return $transitionBlockerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We prefer to return transitions blocker by something else than
|
||||||
|
// marking. Because it means the marking was OK. Transitions are
|
||||||
|
// deterministic: it's not possible to have many transitions enabled
|
||||||
|
// at the same time that match the same marking with the same name
|
||||||
|
if (!$transitionBlockerList->has(TransitionBlocker::BLOCKED_BY_MARKING)) {
|
||||||
|
return $transitionBlockerList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"type": "library",
|
"type": "library",
|
||||||
"description": "Symfony Workflow Component",
|
"description": "Symfony Workflow Component",
|
||||||
"keywords": ["workflow", "petrinet", "place", "transition", "statemachine", "state"],
|
"keywords": ["workflow", "petrinet", "place", "transition", "statemachine", "state"],
|
||||||
"homepage": "http://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Symfony Community",
|
"name": "Symfony Community",
|
||||||
"homepage": "http://symfony.com/contributors"
|
"homepage": "https://symfony.com/contributors"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
|
Reference in New Issue
Block a user