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

Thank you!





https://phpc.social/@dbu

David Buchmann, Liip SA