Skip to content
docuconf
docuconf on GitHub

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

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); Laravel discovers the service provider.

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

2.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.

config/orders.php
<?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.

routes/web.php
// 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 serve

With 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.

Terminal
$ 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 set

5.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:

tests/ConfigTest.php
<?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.php

6.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
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

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