Skip to content
docuconf
docuconf on GitHub

Get started / PHP (Symfony)

docuconf for PHP (Symfony)

Declare the variables in config/packages/docuconf.yaml, written as in the contract, and read them with the docuconf env processor instead of int: and bool:, or from the Docuconf\Values service. The kernel checks them all at boot.

Builds on
Symfony config + %env()% processors
Package
docuconf/docuconf (Packagist)
Requires
PHP 8.2, Symfony 6.4 to 8
Status
v0.1 alpha, not yet released · conformance 134 of 134
Source
docuconf-php · the orders example

Not published yet

Every docuconf SDK is a v0.1 alpha and none is on a package registry yet, so step 1 installs from the main branch. After the first release it will be composer require docuconf/docuconf.

The code below is the orders service, the example every SDK ships: six variables, one of them a secret, checked the same way in every language. Compare it across languages.

1.Install

The package is not on Packagist yet. Install it from GitHub (PHP 8.2+, with mbstring), then register Docuconf\Symfony\DocuconfBundle in config/bundles.php: Flex does not register it yet.

composer config repositories.docuconf vcs https://github.com/Docuconf/docuconf-php
composer require docuconf/docuconf:dev-main

2.Declare your configuration

The variables, in Symfony config, as they appear in the contract. Symfony checks the keys itself, so a typo such as secert: true is an Unrecognized option error when the container compiles. %env(docuconf:PORT)% reads the checked, typed value.

config/packages/docuconf.yaml
# Every variable the service reads, written as in contract.cue. Read them
# with %env(docuconf:NAME)%: the value docuconf validated, typed.
docuconf:
    name: orders-symfony
    vars:
        PORT: {type: int, description: HTTP listen port, min: 1, max: 65535, default: 8080}
        LOG_LEVEL: {type: enum, description: Minimum log level, values: [debug, info, warn, error], default: info}
        DATABASE_URL: {type: url, description: Orders database connection string, required: true, secret: true, schemes: [postgres, postgresql], maxLength: 2048}
        ALLOWED_ORIGINS: {type: list, description: Origins allowed to call the API (CORS), items: string, minItems: 1, default: ['http://localhost:3000']}
        REQUEST_TIMEOUT: {type: duration, description: Timeout for each request, min: 1s, max: 5m, default: 30s}
        WORKER_COUNT: {type: int, description: Number of background workers, min: 1, max: 64, default: 4}

parameters:
    orders.port: '%env(docuconf:PORT)%'
    orders.request_timeout_seconds: '%env(docuconf_seconds:REQUEST_TIMEOUT)%'

3.Load it at boot

The bundle validates when the kernel boots: on web requests, and for every console command except those in docuconf.skip_commands (cache:*, secrets:*, debug:*). Inject Docuconf\Values for the typed values; redacted() hides the secret.

src/Kernel.php
// The typed configuration, with the secret shown as "***".
#[Route('/config')]
public function config(Values $config): JsonResponse
{
    return new JsonResponse($config->redacted());
}

4.Run it, and see an error

Run the example with a valid environment:

DATABASE_URL=postgres://orders:pw@localhost:5432/orders php -S 127.0.0.1:8080 -t public

With PORT=0 and no DATABASE_URL, bin/console docuconf:check lists every problem with its error code and exits 1. A web request fails the same way when the kernel boots, with the report in the server log, and so does every console command that runs the app.

Terminal
$ PORT=0 bin/console docuconf:check
docuconf: 2 configuration problems:
  - PORT [out_of_range]: must be at least 1 (got "0")
  - DATABASE_URL [missing_required]: required, but not set

5.Test your configuration

A KernelTestCase gets the Docuconf service and checks an explicit environment. The kernel still validates the real environment when it boots, so the test run gets a valid one; framework.test: true provides the test container.

tests/ConfigTest.php
<?php

use Docuconf\Symfony\Docuconf;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

// check() takes an explicit environment: the process environment is not read or changed.
final class ConfigTest extends KernelTestCase
{
    public function testAcceptsAValidEnvironment(): void
    {
        $result = self::getContainer()->get(Docuconf::class)->check(['DATABASE_URL' => 'postgres://orders@db/orders']);
        self::assertSame([], $result->violations);
    }

    public function testReportsEveryProblem(): void
    {
        $result = self::getContainer()->get(Docuconf::class)->check(['PORT' => '70000']);
        self::assertSame(
            ['PORT:out_of_range', 'DATABASE_URL:missing_required'],
            array_map(fn ($v) => "{$v->input}:{$v->code}", $result->violations),
        );
    }
}
config/packages/test/framework.yaml
# The test container, for KernelTestCase.
framework:
    test: true
DATABASE_URL=postgres://ci@db/orders APP_ENV=test KERNEL_CLASS='App\Kernel' vendor/bin/phpunit tests/ConfigTest.php

6.Export the contract

bin/console docuconf:export writes the contract from the bundle configuration. --check fails when the committed file is out of date, for CI.

bin/console docuconf:export --output=contract.cue
contract.cue
// Code generated by docuconf. DO NOT EDIT.
package orders_symfony

import "docuconf.dev/contract"

contract.#Contract & {
	apiVersion: "docuconf.dev/v1alpha1"
	kind:       "ConfigContract"
	metadata: {
		name: "orders-symfony"
		generator: {
			language: "php"
			sdk:      "docuconf/docuconf"
			version:  "0.1.0"
		}
	}
	vars: {
		ALLOWED_ORIGINS: {
			type:        "list"
			description: "Origins allowed to call the API (CORS)"
			items:       "string"
			encoding:    "csv"
			separator:   ","
			minItems:    1
			default: ["http://localhost:3000"]
		}
		DATABASE_URL: {
			type:        "url"
			description: "Orders database connection string"
			required:    true
			secret:      true
			schemes: ["postgres", "postgresql"]
			maxLength: 2048
		}
		LOG_LEVEL: {
			type:        "enum"
			description: "Minimum log level"
			values: ["debug", "info", "warn", "error"]
			default: "info"
		}
		PORT: {
			type:        "int"
			description: "HTTP listen port"
			min:         1
			max:         65535
			default:     8080
		}
		REQUEST_TIMEOUT: {
			type:        "duration"
			description: "Timeout for each request"
			encoding:    "go"
			min:         "1s"
			max:         "5m"
			default:     "30s"
		}
		WORKER_COUNT: {
			type:        "int"
			description: "Number of background workers"
			min:         1
			max:         64
			default:     4
		}
	}
}

The platform then checks each environment's values against contract.cue before it deploys, with docuconf vet, the Helm chart or CUE. See How it works.

7.Symfony

Values are read the way %env(NAME)% reads them: real environment variables, .env files, the secrets vault (bin/console secrets:set DATABASE_URL) and env(NAME) parameter defaults. Put the processor innermost when chaining: %env(default:fallback:docuconf:PORT)%. presets: [symfony] also declares APP_SECRET, APP_ENV and APP_DEBUG, and declaration: can point at a PHP file for file inputs. See the README.

Next

Every snippet on this page is compiled or run against docuconf-php's main branch in CI. Facts checked 2026-10-08.