Get started / PHP (Laravel)
docuconf for PHP (Laravel)
In config/*.php, Env::int(...), Env::url(...) and the other helpers replace env(): each returns the typed value where env() did, and records the variable for the boot check and the contract.
- Builds on
- Laravel config + vlucas/phpdotenv
- Package
- docuconf/docuconf (Packagist)
- Requires
- PHP 8.2, Laravel 11 to 13
- Status
- v0.1 alpha, not yet released · conformance 134 of 134
- Source
- docuconf-php · the orders example
Not published yet
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); Laravel discovers the service provider.
composer config repositories.docuconf vcs https://github.com/Docuconf/docuconf-php
composer require docuconf/docuconf:dev-main2.Declare your configuration
Each variable is declared where the app already reads it. Env::int, Env::enum, Env::url, Env::list and Env::duration take the name, a description and the rules. A PHPDoc comment above an entry can give the description (its first paragraph) and the details (the rest) instead, as for REQUEST_TIMEOUT. The log level is ORDERS_LOG_LEVEL because Laravel's own config/logging.php already reads LOG_LEVEL.
<?php
use Docuconf\Laravel\Env;
// Env:: is env() with a type, rules and a description. Each call returns the
// typed value, and the app refuses to boot if any of them is wrong.
return [
'port' => Env::int('PORT', 'HTTP listen port', default: 8080, min: 1, max: 65535),
// ORDERS_, not LOG_LEVEL: Laravel's own config/logging.php reads LOG_LEVEL.
'log_level' => Env::enum('ORDERS_LOG_LEVEL', 'Minimum level the orders code logs', ['debug', 'info', 'warn', 'error'], default: 'info'),
// A secret: never printed, and the platform must supply it from a Secret.
'database_url' => Env::url('DATABASE_URL', 'Orders database connection string', required: true, schemes: ['postgres', 'postgresql'], secret: true, maxLength: 2048),
'allowed_origins' => Env::list('ALLOWED_ORIGINS', 'Origins allowed to call the API (CORS)', default: ['http://localhost:3000'], minItems: 1),
// A Docuconf\Duration; written "30s", "1m30s" in the env. The PHPDoc
// comment documents it: its first paragraph is the description, and the
// rest is exported as details, for `docuconf docs`.
/**
* Timeout for each request.
*
* Raise it when clients upload large order batches. Keep it below the
* load balancer's idle timeout, or the client sees a reset rather than
* a `504`.
*/
'request_timeout' => Env::duration('REQUEST_TIMEOUT', default: '30s', min: '1s', max: '5m'),
'worker_count' => Env::int('WORKER_COUNT', 'Number of background workers', default: 4, min: 1, max: 64),
];3.Load it at boot
Nothing to call: the service provider checks every recorded variable when the app boots, so php artisan serve, queue:work, migrate and every request refuse to run on a bad configuration. Docuconf\Values holds the typed values, and redacted() is safe to serve.
// The typed configuration, with the secret shown as "***".
Route::get('/config', fn (Values $config) => $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 artisan serveWith PORT=0 and no DATABASE_URL, php artisan serve does not start: it lists every problem with its error code and exits 1. The same text goes to /dev/termination-log, and php artisan docuconf:check prints it without starting anything.
$ PORT=0 php artisan serve
docuconf: 2 configuration problems:
- PORT [out_of_range]: must be at least 1 (got "0")
- DATABASE_URL [missing_required]: required, but not set5.Test your configuration
With APP_ENV=testing the app skips its boot check, and check() takes an explicit environment, so a test never depends on the process environment. A PHPUnit test on Laravel's own TestCase:
<?php
use Docuconf\Declaration;
use Illuminate\Foundation\Testing\TestCase;
// With APP_ENV=testing the app skips its boot check; check() takes an explicit environment instead.
final class ConfigTest extends TestCase
{
public function test_accepts_a_valid_environment(): void
{
$result = $this->app->make(Declaration::class)->check(['DATABASE_URL' => 'postgres://orders@db/orders']);
$this->assertSame([], $result->violations);
}
public function test_reports_every_problem(): void
{
$result = $this->app->make(Declaration::class)->check(['PORT' => '70000']);
$this->assertSame(
['PORT:out_of_range', 'DATABASE_URL:missing_required'],
array_map(fn ($v) => "{$v->input}:{$v->code}", $result->violations),
);
}
}APP_ENV=testing vendor/bin/phpunit tests/ConfigTest.php6.Export the contract
php artisan docuconf:export writes the contract without validating the environment. --check fails when the committed file is out of date, for CI.
php artisan docuconf:export --output=contract.cue// Code generated by docuconf. DO NOT EDIT.
package orders
import "docuconf.dev/contract"
contract.#Contract & {
apiVersion: "docuconf.dev/v1alpha1"
kind: "ConfigContract"
metadata: {
name: "orders"
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
}
ORDERS_LOG_LEVEL: {
type: "enum"
description: "Minimum level the orders code logs"
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"
details: "Raise it when clients upload large order batches. Keep it below the\nload balancer's idle timeout, or the client sees a reset rather than\na `504`."
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.Laravel
Run php artisan config:cache when the container starts, not when the image is built: docuconf records what each Env:: call returned, and the boot check fails on a cache built from other values. 'presets' => ['laravel'] in config/docuconf.php also declares the variables Laravel reads itself, such as APP_KEY and DB_URL. A plain PHP app declares with Env::declare(), in phpdotenv's style. See the README.
Next
- The docuconf-php README: every type, file input and option.
- The orders example, with its smoke test.
- The same service in every language, side by side.
- What every SDK supports, and this one's known gaps: No profiles or config-file overlays. No reload: watch. Run php artisan config:cache when the container starts, not in the image build.
Every snippet on this page is compiled or run against docuconf-php's main branch in CI. Facts checked 2026-10-08.