Developer Experience (DX) refers to how smooth and efficient it feels to work with a framework or tool. In Symfony 7.4, we've added many small DX improvements, and this post highlights some of them.

Question Helper Timeout

Jan Nedbal
Contributed by Jan Nedbal in #61092

The Question helper from the Console component provides utilities to ask users for input. In Symfony 7.4, you can now set a timeout for questions. If the user doesn't respond within the specified time, a MissingInputException will be thrown:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Console\Exception\MissingInputException;
use Symfony\Component\Console\Question\ConfirmationQuestion;

// ...
public function __invoke(InputInterface $input, OutputInterface $output): int
{
    // ...
    $helper = new QuestionHelper();

    $question = new ConfirmationQuestion('Restart the server?', false);
    $question->setTimeout(10); // user must confirm in 10 seconds or less
}

Enum Type Guesser for Forms

Matthias Schmidt
Contributed by Matthias Schmidt in #61297

The Symfony Form component uses type guessers to decide which form types to use for fields that don't define their type explicitly. In Symfony 7.4, we added a new type guesser for PHP enums.

You no longer need to define enum fields manually. Symfony will automatically use the EnumType class and set its class option whenever it finds an enum field.

New Command to Generate OIDC Tokens

Hubert Lenoir
Contributed by Hubert Lenoir in #60660

In Symfony 6.3, we introduced an OpenID Connect Token Handler that allows you to decode JWT tokens, validate them, and extract user information. In Symfony 7.4, we're adding a command to generate JWTs, which is useful for testing and development:

1
2
3
4
5
6
$ php bin/console security:oidc-token:generate jane.doe@example.com \
      --firewall="api" \
      --algorithm="HS256" \
      --issuer="https://example.com" \
      --ttl=7200 \
      --not-before=tomorrow

Default Locale Outside HTTP Context

Mathieu
Contributed by Mathieu in #62010

When generating URLs outside the HTTP context, there are special challenges, such as defining the hostname or locale. In Symfony 7.4, the kernel.default_locale parameter is now also used as the default locale when generating URLs outside HTTP requests.

New Helpers for BrowserKit

Santiago San Martin
Contributed by Santiago San Martin in #60895 and #60955

The BrowserKit component simulates the behavior of a web browser, allowing you to make requests, click links, and submit forms programmatically. In Symfony 7.4, it adds two new helpers:

  • isFirstPage(): returns true if the current position is at the start of the history stack.
  • isLastPage(): returns true if the current position is at the end of the history stack.

These are especially useful in tests, so we also added a few new PHPUnit constraints:

1
2
3
4
$this->assertBrowserHistoryIsOnFirstPage();
$this->assertBrowserHistoryIsNotOnFirstPage();
$this->assertBrowserHistoryIsOnLastPage();
$this->assertBrowserHistoryIsNotOnLastPage();

Better Dumps in non-HTML Contexts

Alexandre Daubois
Contributed by Alexandre Daubois in #58070

In Symfony 7.4, we improved exceptions in the terminal to avoid rendering them as HTML. Similarly, the dd() and dump() functions now only render HTML when the request contains an Accept HTTP header with an html value. In all other cases (e.g. API debugging, terminal output), dumps are rendered as plain text.

Simpler Target Attributes

Valmont Pehaut-Pietri
Contributed by Valmont Pehaut-Pietri in #60874

When dealing with multiple implementations of the same type, the #[Target] attribute helps you select which one to inject. In Symfony 7.4, we've simplified how you can define those values.

You can now use the same names as in your configuration files, without adding extra suffixes based on the service type:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Before - Asset Packages
#[Target('foo_package.package')] private PackageInterface $package
// After
#[Target('foo_package')] private PackageInterface $package

// Before - Lock Factories
#[Target('invoice.lock.factory')] private LockFactory $lockFactory
// After
#[Target('invoice')] private LockFactory $lockFactory

// Before - Rate Limiters
#[Target('anonymous_api.limiter')] RateLimiterFactoryInterface $rateLimiter
// After
#[Target('anonymous_api')] RateLimiterFactoryInterface $rateLimiter
Published in #Living on the edge