Building PHP Applications with the Symfony Framework

in 2026





ConFoo, Montreal, Canada - February 26th, 2026

© David Buchmann







David Buchmann - david@liip.ch

PHP Engineer, Liip SA, Switzerland

Code Quality in the Age of LLMs

Dokumentation

Automate as much of your documentation as possible:
Dockerfile, composer scripts, Makefile, deployment



PHP Language Features

Strict Typing

/**
 * @param string $message the message
 * @return bool
 */
public function confirm($question)
{
    ...
}
    

Please type your methods and properties

public function confirm(string $question): bool
{
    ...
}

Union Types

public function get(string|Uuid $id): MyModel
{
    if (is_string($id)) {
        trigger_error(
            'Pass Uuid instance for the id.',
            E_USER_DEPRECATED
        );
        $id = new Uuid($id);
    }
}
            

Use Value Objects

Value Object example

final class Money {
    public function __construct(
        private int $amount,
        private string $currency,
    ) {}
    public function setAmountInCurrency(
        int $amount,
        string $currency
    ): self {
        $this->amount = $amount;
        $this->currency = $currency;
    }
}

Enums

enum QuestionType: string
{
    case Boolean = 'boolean';
    case Checkboxes = 'checkboxes';
    case Numeric = 'numeric';
    case Range = 'range';
}

Data Transfer Objects (DTO)

PHP Named Arguments

Benefits
Problem: Argument name becomes part of the BC promise

Named Arguments: Readability

setcookie(
    'session',
    'myvalue',
    0,
    '',
    '',
    false,
    true
);
                

Named Arguments: Readability

setcookie(
  name: 'session',
  value: 'myvalue',
  expires_or_options: 0,
  path: '',
  domain: '',
  secure: false,
  httponly: true,
);
                

Named Arguments: Readability

setcookie(
  name: 'session',
  value: 'myvalue',
  expires_or_options: 0, // default
  path: '', // default
  domain: '', //default
  secure: false, // default
  httponly: true,
);
                

Named Arguments: Skip default

setcookie(
  name: 'session',
  value: 'myvalue',
  expires_or_options: 0,
  path: '',
  domain: '',
  secure: false,
  httponly: true,
);
                
setcookie(
  name: 'session',
  value: 'myvalue',
  httponly: true,
);



// skip default arguments
            



Annotations => Attributes

PHP Attributes


class Controller
{
-    /** @Route('/home') */
+    #[Route('/home')]
    public function index()
    {
        ...
    }
}
            

Attributes and named parameters

Attributes are PHP code: syntax errors, typo detection, constants, named arguments etc
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

#[Route(
    path: '/home',
    name: 'homepage',
    methods: [Request::METHOD_GET]],
)]
public function index(): Response
{...

Named argument in Symfony Validator

Symfony validator attributes with "options" array:
#[Assert\Length([
  'min' => 8,
  'max' => 50,
  'minMessage' => 'Password needs at least {{ limit }} chars',
  'maxMessage' => 'Password cannot exceed {{ limit }} chars',
])]

Named argument in Symfony Validator

Validator nowadays also has named parameters.
#[Assert\Length(
  min: 8,
  max: 50,
  minMessage: 'Password needs at least {{ limit }} chars',
  maxMessage: 'Password cannot exceed {{ limit }} chars',
)]



Tools

Tools in your CI


phpunit

phpunit testing strategy


Smoke
  • Simulate web request
  • Quick to write
  • Not very useful for debugging
Functional
  • Fetch service from container
  • Mock clients to external service
  • Use fixtures in database (but don't mock database)
Unit
  • Very fast
  • For business logic
  • Test all edge cases
  • Still don't mock DTO / Value Objects

phpstan

The missing «compiler» for PHP - discovers mistakes in CI

Alternatives: Psalm, Phan, Exakat

phpstan levels

phpstan levels

phpstan levels

rector

Alternatives: RefactorPHP, Phpactor

phpmd

PHP mess detector - identify code with «smells»

phpmd: cyclomatic complexity

function getDiscount(User $user): int
{
    if ($user->isLoggedIn()) {
        if ($user->isPremium()) {
            return 20;
        }
        return 10;
    }

    return 0;
}

phpmd: early return

function getDiscount(User $user): int
{
    if (!$user->isLoggedIn()) {
        return 0;
    }

    if ($user->isPremium()) {
        return 20;
    }

    return 10;
}

php-cs-fixer

Alternatives: PHP_CodeSniffer, Prettier (with PHP Plugin), phpfmt



Symfony Framework

Symfony Maker

Symfony Autowiring

(Can get rather magic, love it or hate don't use it)

Symfony Autowiring


# config/packages/framework.yaml
framework:
    http_client:
        scoped_clients:
        githubClient:
            base_uri: 'https://api.github.com'

        stripeClient:
            base_uri: 'https://api.stripe.com'

Symfony Autowiring


use Symfony\Contracts\HttpClient\HttpClientInterface;

class ApiService
{
    public function __construct(
        HttpClientInterface $githubClient,
        HttpClientInterface $stripeClient,
    ) {}
}
            

Symfony Autoconfigure

Value Resolvers in Controller methods

Value Resolvers


Some (not yet all) of this also in Symfony Command



Conclusions

Code Quality in the Age of LLMs

Shoutouts