Rename Client to Browser Contributed by Fabien Potencier in #30541 use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class UserTest extends WebTestCase { public function testSomething() { // before: Symfony\Bundle\FrameworkBundle\Client // after: Symfony\Bundle\FrameworkBundle\KernelBrowser $browser = $this->createClient(); } }
Deprecated all PSR-16 adapters Contributed by Nicolas Grekas in #29236 // before $cache = new FilesystemCache(); // after $cache = new Psr16Cache(new FilesystemAdapter());
Deprecated SimpleCacheAdapter Contributed by Nicolas Grekas in #29236 /** @var \Psr\SimpleCache\CacheInterface $psr16Cache */ // before $cache = new SimpleCacheAdapter($psr16Cache); // after $cache = new Psr16Adapter($psr16Cache);
Deprecated the „Psr\SimpleCache\CacheInterface" and „cache.app.simple" service Contributed by Nicolas Grekas in #29236 // before use Psr\SimpleCache\CacheInterface; class FooService { private $cache; public function __construct(CacheInterface $cache) { $this->cache = $cache; } public function doFoo() { $value = $this->cache->get('foo'); if (null === $value) { // compute value here $this->cache->set('foo', $value); } } }
Deprecated the „Psr\SimpleCache\CacheInterface" and „cache.app.simple" service Contributed by Nicolas Grekas in #29236 // after use Symfony\Contracts\Cache\CacheInterface; class FooService { private $cache; public function __construct(CacheInterface $cache) { $this->cache = $cache; } public function doFoo() { $value = $this->cache->get('foo', function () { // compute value here }); } }
Deprecated the „Psr\SimpleCache\CacheInterface" and „cache.app.simple" service Contributed by Nicolas Grekas in #29236 services: app.simple_cache: class: Symfony\Component\Cache\Psr16Cache arguments: - '@cache.app' App\FooService: arguments: - ‘@app.simple_cache'
Deprecated support for non-string default env() parameters Contributed by Roland Franssen in #27808 # before parameters: env(NAME): 1.5 # after parameters: env(NAME): '1.5'
Deprecate default usage of “putenv()” Contributed by Tobias Nyholm in #31062 // before $dotenv = new Dotenv(); // after $dotenv = new Dotenv($usePutenv = true); // if you don't need to read env vars with getenv() $dotenv = new Dotenv($usePutenv = false);
Simplier event dispatching $order = new Order(); $newOrderEvent = new OrderPlacedEvent($order); // before $dispatcher->dispatch(OrderEvents::NEW_ORDER, $newOrderEvent); // after $dispatcher->dispatch($newOrderEvent, OrderEvents::NEW_ORDER); Contributed by Nicolas Grekas in #28920 Changed signature of EventDispatcherInterface::dispatch()
Simplier event dispatching class StoreSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { // before return [ OrderEvents::NEW_ORDER => 'onStoreOrder', ]; // after return [ OrderPlacedEvent::class => 'onStoreOrder', ]; } } Contributed by Nicolas Grekas in #28920 Subscribe to events using the events FQCN
Simplier event dispatching Contributed by Nicolas Grekas in #28920 Optional Event name in EventDispatcherInterface::dispatch() // before $dispatcher->dispatch(OrderEvents::NEW_ORDER, $newOrderEvent); // after $dispatcher->dispatch($newOrderEvent); // Symfony\Contracts\EventDispatcher\EventDispatcherInterface.php public function dispatch($event/*, string $eventName = null*/);
Supporting both dispatchers use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; class CustomController { public function __invoke(EventDispatcherInterface $dispatcher) { $this->dispatcher = LegacyEventDispatcherProxy::decorate($dispatcher); } public function someMethod() { $this->dispatcher->dispatch(OrderEvents::NEW_ORDER, $newOrderEvent); $this->dispatcher->dispatch($newOrderEvent, OrderEvents::NEW_ORDER); $this->dispatcher->dispatch($newOrderEvent); } }
Deprecated the MIME type guessing classes Contributed by Fabien Potencier in #29896 // before use Symfony\Component\HttpFoundation\File\MimeType as HttpFoundation; $guesser = new HttpFoundation\FileinfoMimeTypeGuesser(); $mimeType = $guesser->guess($filename); $guesser = new HttpFoundation\MimeTypeExtensionGuesser(); $extension = $guesser->guess($filename); // after use Symfony\Component\Mime; $guesser = new Mime\FileinfoMimeTypeGuesser(); $mimeType = $guesser->guess($filename); $guesser = new Mime\MimeTypes(); $extensions = $guesser->getExtensions($filename);
The Role and SwitchUserRole classes are deprecated Contributed by Christian Flothmann in #22048 class User implements UserInterface { // before public function getRoles(): array { return [new Role('ROLE_USER')]; } // after public function getRoles(): array { return ['ROLE_USER']; } }
The Role and SwitchUserRole classes are deprecated Contributed by Christian Flothmann in #22048 // before $isSwitched = false; foreach ($token->getUser()->getRoles() as $role) { if ($role instanceof SwitchUserRole) { $isSwitched = true; } } // after $isSwitched = $token instanceof SwitchUserToken;
The Role and SwitchUserRole classes are deprecated Contributed by Christian Flothmann in #22048 // before $roleNames = []; foreach ($token->getRoles() as $role) { $roleNames[] = $role->getRole(); } // after $roleNames = $token->getRoleNames();
The Role and SwitchUserRole classes are deprecated Contributed by Christian Flothmann in #22048 // before $reachableRoles = $roleHierarchy->getReachableRoles( $user->getRoles() ); // after $reachableRoles = $roleHierarchy->getReachableRoleNames( $user->getRoleNames() );
The Argon2iPasswordEncoder and BCryptPasswordEncoder class have been deprecated Contributed by Robin Chalas in #31019 and Nicolas Grekas in #31170 // before $encoder = new Argon2iPasswordEncoder(); $encoder->encodePassword('foo', null); $encoder = new BCryptPasswordEncoder(13); $encoder->encodePassword('bar', null); // after $encoder = new SodiumPasswordEncoder(); $encoder->encodePassword('foo', null); $encoder = new NativePasswordEncoder(); $encoder->encodePassword('bar', null);
Configuring encoders using argon2i or bcrypt as algorithm has been deprecated Contributed by Robin Chalas in #31019 and Nicolas Grekas in #31170 security: # before encoders: App\Entity\User: algorithm: bcrypt cost: 12 App\Security\User: argon2i # after encoders: App\Entity\User: auto App\Security\User: auto
EnvProcessor: default # config/services.yaml parameters: # if PRIVATE_KEY is not a valid file path, # the content of raw_key is returned private_key: '%env(default:raw_key:file:PRIVATE_KEY)%' raw_key: '%env(PRIVATE_KEY)%' Contributed by Jérémy Derussé in #28976
EnvProcessor: trim Contributed by … in … # config/services.yaml parameters: private_key: '%env(trim:file:PRIVATE_KEY)%' Contributed by Maxime Steinhausser in #29781
Translation Improvements Contributed by Webnet team in #28635 $builder->add('accept_terms', CheckboxType::class, [ 'label' => 'Comment for the order of %company_name%’, 'label_translation_parameters' => [ '%company_name%' => 'Acme', ], 'help' => ‘Help text for the order of %company_name%’, 'help_translation_parameters' => [ '%company_name%' => 'Acme', ], 'attr_translation_parameters' => [ '%company_name%' => ‘Acme', ], ]);
Date Types: input_format use Symfony\Component\Form\Extension\Core\Type\DateTimeType; $builder->add('startsAt', DateTimeType::class, [ // ..., 'input' => 'string', 'input_format' => 'm-d H:i', ]); Contributed by Thomas Calvet in #29887
NumberType: String Input Contributed by Bernhard Schussek in #30893 use Doctrine\ORM\Mapping as ORM; /** * @\Entity */ class Product { /** * @ORM\Column(type="decimal") */ private $price; // ... }
NumberType: String Input Contributed by Bernhard Schussek in #30893 $builder->add('price', NumberType::class, [ 'input' => 'string', // default is "number" ]);
Transformation Failure Messages Contributed by Maxime Steinhausser in #20978 // before class MoneyDataMapper implements DataMapperInterface { // ... public function mapFormsToData($forms, &$data) { $forms = iterator_to_array($forms); if (!is_numeric($forms['amount']->getData())) { throw new TransformationFailedException('Expected numeric value'); } } } // -> "This value is not valid."
Transformation Failure Messages Contributed by Maxime Steinhausser in #20978 // after class MoneyDataMapper implements DataMapperInterface { // ... public function mapFormsToData($forms, &$data) { $forms = iterator_to_array($forms); if (!is_numeric($forms['amount']->getData())) { $failure = new TransformationFailedException('Expected numeric value'); $failure->setInvalidMessage('Money amount should be numeric. {{ amount }} is invalid.', [ '{{ amount }}' => json_encode($forms['amount']->getData()) ]); throw $failure; } } } // -> "Money amount should be numeric. "foo" is invalid."
TimezoneType: Intl Support Contributed by Roland Franssen in #31195 // before: $builder->add('timezone', TimezoneType::class, [ 'input' => 'string', ]);
TimezoneType: Intl Support Contributed by Roland Franssen in #31195 // after $builder->add('timezone', TimezoneType::class, [ 'input' => 'intltimezone', ]);
Simplified API Contributed by Roland Franssen in #28846 // before use Symfony\Component\Intl\Intl; \Locale::setDefault('en'); $countries = Intl::getRegionBundle()->getCountryNames(); // => ['AF' => 'Afghanistan', ...] $country = Intl::getRegionBundle()->getCountryName('GB'); // => 'United Kingdom'
Simplified API Contributed by Roland Franssen in #28846 // after use Symfony\Component\Intl\Countries; $countries = Countries::getNames(); // => ['AF' => 'Afghanistan', ...] $country = Countries::getName('GB'); // => 'United Kingdom'
Transport: Doctrine Transport Contributed by Vincent Touzet in #29007 # .env MESSENGER_TRANSPORT_DSN=doctrine://default # config/packages/messenger.yaml framework: messenger: transports: async_priority_high: "%env(MESSENGER_TRANSPORT_DSN)%?queue_name=high" async_normal: dsn: "%env(MESSENGER_TRANSPORT_DSN)%" options: queue_name: normal
Transport: Doctrine Transport Contributed by Vincent Touzet in #29007 Option Description Default table_name Name of the table messenger_messages queue_name Name of the queue (a column in the table, to use one table for multiple transports) default redeliver_timeout Timeout before retrying a message in seconds. 3600 auto_setup Create the db table when the transport is first used. TRUE
Transport: Redis Transport Contributed by Alexander Schranz in #30917 # .env MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages # config/packages/messenger.yaml framework: messenger: transports: async_priority_normal: "%env(MESSENGER_TRANSPORT_DSN)%"
Transport: Redis Transport Contributed by Alexander Schranz in #30917 Option Description Default Stream The Redis stream name messages Group The Redis consumer group name symfony Consumer Consumer name used in Redis consumer Serializer How to serialize the final payload in Redis (the Redis::OPT_SERIALIZER option) Redis::SERIALIZER_PHP
Transport: In Memory Transport Contributed by Gary PEGEOT and Samuel ROZE in #29097 # config/packages/test/messenger.yaml framework: messenger: transports: async_priority_normal: 'in-memory:///'
Transport: In Memory Transport Contributed by Gary PEGEOT and Samuel ROZE in #29097 class DefaultControllerTest extends WebTestCase { public function testSomething() { $client = static::createClient(); // ... $this->assertSame(200, $client->getResponse()->getStatusCode()); /* @var InMemoryTransport $transport */ $transport = self::$container->get('messenger.transport.async_priority_normal'); $this->assertCount(1, $transport->get()); } }
Transport: Sync Transport Contributed by Ryan Weaver in #30759 # config/packages/messenger.yaml framework: messenger: transports: async: '%env(MESSENGER_TRANSPORT_DSN)%' routing: 'App\Message\SmsNotification': async 'App\Message\OtherMessage': async # .env by default, handle this sync MESSENGER_TRANSPORT_DSN=sync:// # .env.local on production (or set this via real env vars) MESSENGER_TRANSPORT_DSN=amqp://...
Retry strategy Contributed by Ryan Weaver in #30557 # config/packages/messenger.yaml framework: messenger: transports: async_priority_high: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' retry_strategy: max_retries: 3 delay: 1000 # ms delay multiplier: 2 # delay multiplier before each retry max_delay: 0 # max ms a retry should ever be delayed # 0 => infinite # override all of this with a service that implements # Symfony\Component\Messenger\Retry\RetryStrategyInterface # service: null
Failure Transport support Contributed by ... in #... Contributed by Ryan Weaver in #30970 # config/packages/messenger.yaml framework: messenger: # after retrying, messages will be sent # to the "failed" transport failure_transport: failed transports: # ... other transports failed: 'doctrine://default?queue_name=failed'
Improved Commands Contributed by Vincent Touzet in #29476 $ php bin/console messenger:setup-transports [transport] Contributed by Ryan Weaver in #30754 $ php bin/console messenger:stop-workers $ bin/console messenger:consume amqp_high amqp_medium amqp_low Contributed by Ryan Weaver in #30708 New New
Native php serialize() support for transports Contributed by Ryan Weaver and Christian Flothmann in 29958 Messages are now serialized using PHP’s native serialize() & unserialize() functions. Symfony Serializer not the default serializer anymore.
Making the serializer configurable by transport Contributed by Ryan Weaver in #30628 # config/packages/messenger.yaml framework: messenger: serializer: default_serializer: messenger.transport.symfony_serializer symfony_serializer: format: json context: { } transports: async_priority_normal: dsn: # ... serializer: messenger.transport.symfony_serializer
WorkerMessageEvents Contributed by Ryan Weaver in #30557 namespace Symfony\Component\Messenger\Event; class WorkerMessageFailedEvent extends AbstractWorkerMessageEvent { public function getThrowable(): \Throwable; public function willRetry(): bool; } class WorkerMessageHandledEvent extends AbstractWorkerMessageEvent { ... } class WorkerMessageReceivedEvent extends AbstractWorkerMessageEvent { ... }
SendMessageToTransportsEvent Contributed by Ryan Weaver in #30650 class MessageSendToTransportListener implements EventSubscriberInterface { public function onSendMessage(SendMessageToTransportsEvent $event) { $envelope = $event->getEnvelope(); if (!$envelope->getMessage() instanceof SomeMailerMessage) { return; } $envelope->with(new AmpqRoutingKeyStamp('mailer-route')) $event->setEnvelope($envelope); } public static function getSubscribedEvents() { return [SendMessageToTransportsEvent::class => 'onSendMessage']; } }
WorkerStoppedEvent Contributed by Robin Chalas in #31282 use Symfony\Component\Messenger\Event\WorkerStoppedEvent; class WorkerStoppedListener implements EventSubscriberInterface { public static function getSubscribedEvents() { return [WorkerStoppedEvent::class => 'onWorkerStopped',]; } public function onWorkerStopped(WorkerStoppedEvent $workerStoppedEvent) { // e.g. do some cleanup or other business tasks } }
New PHPUnit assertions for the WebTestCase Contributed by Fabien Potencier and Alex Rock in #30813 // before use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class DefaultControllerTest extends WebTestCase { public function testSomething() { $client = static::createClient(); $crawler = $client->request('GET', '/test'); $this->assertSame(200, $client->getResponse()->getStatusCode()); $this->assertContains('Hello World', $crawler->filter('h1')->text()); } }
New PHPUnit assertions for the WebTestCase Contributed by Fabien Potencier and Alex Rock in #30813 // after use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class DefaultControllerTest extends WebTestCase { public function testSomething() { $client = static::createClient(); $crawler = $client->request('GET', '/test'); $this->assertResponseIsSuccessful(); $this->assertSelectorTextContains('h1', 'Hello World'); } }
New PHPUnit assertions for the WebTestCase Contributed by Fabien Potencier and Alex Rock in #30813 $this->assertResponseIsSuccessful(...) $this->assertResponseStatusCodeSame(...) $this->assertResponseRedirects(...) $this->assertResponseHasHeader(...) $this->assertResponseNotHasHeader(...) $this->assertResponseHeaderSame(...) $this->assertResponseHeaderNotSame(...) $this->assertResponseHasCookie(...) $this->assertResponseNotHasCookie(...) $this->assertResponseCookieValueSame(...) $this->assertSelectorExists(...) $this->assertSelectorNotExists(...) $this->assertSelectorTextContains(...) $this->assertSelectorTextSame(...) $this->assertSelectorTextNotContains(...) $this->assertPageTitleSame(...) $this->assertPageTitleContains(...) $this->assertInputValueSame(...) $this->assertInputValueNotSame(...) $this->assertRequestAttributeValueSame(...) $this->assertRouteSame(...)
DomCrawler Improvement Contributed by Titouan Galopin in #29306 The DomCrawler can make use of the Masterminds/html5-php library to parse HTML5. $ composer req masterminds/html5:^2.6
Extracting Translation Content from PHP Files Contributed by Yonel Ceruto in #30120 # updates the german translation with missing strings $ php bin/console translation:update --dump-messages --force de The translation update command now also extracts translation contents from PHP files. ControllerArguments ServiceArguments ServiceMethodCalls ServiceSubscriber
NotCompromisedPassword Contributed by Kévin Dunglas in #27738 use Symfony\Component\Validator\Constraints as Assert; class User { /** * @Assert\NotCompromisedPassword */ protected $rawPassword; }
Json Constraint Contributed by Imad Zairig in #28477 use Symfony\Component\Validator\Constraints as Assert; class Book { /** * @Assert\Json(message = "This is not valid JSON") */ protected $chapters; }
Unique Constraint Contributed by Yevgeniy Zholkevskiy in #26555 use Symfony\Component\Validator\Constraints as Assert; class Person { /** * @Assert\Unique( * message="The {{ value }} email is repeated." * ) */ protected $contactEmails; }
Number Contraints Contributed by Jan Schädlich in #28637 use Symfony\Component\Validator\Constraints as Assert; class Settings { /** * @Assert\Positive * @Assert\PositiveOrZero * @Assert\Negative * @Assert\NegativeOrZero */ protected $value; }
TimeZone Contraints Contributed by Javier Spagnoletti and Hugo Hamon in #30900 use Symfony\Component\Validator\Constraints as Assert; class UserSettings { /** * @Assert\Timezone * @Assert\Timezone(zone=\DateTimeZone::AMERICA) * @Assert\Timezone(zone=\DateTimeZone::PER_COUNTRY, countryCode="CN") */ protected $timezone; }
Automatic Validation Contributed by Kévin Dunglas in #27735 This new feature introspects your doctrine mappings and adds the corresponding validation automatically. Doctrine mapping Automatic validation constraint nullable=false @Assert\NotNull type=... @Assert\Type(...) unique=true @UniqueEntity length=... @Assert\Length(...)
Automatic Validation /** @ORM\Entity */ class SomeEntity { /** @ORM\Column(length=4) */ public $pinCode; } $entity = new SomeEntity(); $entity->setPinCode('1234567890'); $violationList = $validator->validate($entity); var_dump((string) $violationList); // Value too long. It should have 4 chars or less. Contributed by Kévin Dunglas in #27735
Added a context to Workflow::apply() Contributed by Grégoire Pineau in #29146 $workflow->apply($article, ‘published', [ 'time' => date('y-m-d H:i:s'), ]); It’s now possible to pass a custom context when applying a transition. class Article { - public function setMarking($marking) + public function setMarking($marking, $context = []) framework: workflows: article: type: workflow marking_store: - type: multiple_state + type: method
use Symfony\Component\Workflow\Event\TransitionEvent; class TransitionEventSubscriber implements EventSubscriberInterface { public function onWorkflowArticleTransition(TransitionEvent $event) { $context = $event->getContext(); // add some context, e.g. $context['user'] = $user->getUsername(); $event->setContext($context); } public static function getSubscribedEvents() { return [TransitionEvent::class => 'onWorkflowArticleTransition']; } } Allow to modify the context in a listener Contributed by Grégoire Pineau in #30902
Allow to configure many initial places Contributed by Grégoire Pineau in #30468 and #30890 When using a Workflow, it’s possible to have a subject in many places. And now this also works for initial places. # Before workflows: article: type: workflow initial_place: draft places: [draft, in_progress, published] # After workflows: article: type: workflow initial_marking: [draft] places: [draft, in_progress, published] Deprecated