Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Neon Laravel Package

nette/neon

NEON is a human-friendly, YAML-like data serialization format used for Nette configuration files. This package provides a fast, strict parser and dumper for PHP, with support for comments, inheritance, entities and includes, plus detailed error reporting.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer — there is no Laravel service provider to publish:

composer require nette/neon

The entire public surface is in Nette\Neon\Neon:

use Nette\Neon\Neon;

$data = Neon::decode("
mail:
    driver: smtp
    host: mail.example.com
    port: 587
");
// ['mail' => ['driver' => 'smtp', 'host' => 'mail.example.com', 'port' => 587]]

echo Neon::encode($data, blockMode: true);

Where to look first, in order:

  1. Nette\Neon\Neon — three methods only: decode(), decodeFile(), encode(). That is your 90 % API.
  2. Nette\Neon\Exception — every parse failure throws this, with the offending line. Always catch it at the boundary.
  3. Nette\Neon\Linter — validates a string and throws on syntax errors, useful for CI.
  4. Nette\Neon\Node\* and Nette\Neon\Parser — the AST layer, only relevant if you need position info or custom traversal.

First use case in a Laravel app: a feature-flag / tooling config that humans edit. Do not start by ripping out config/*.php — start by reading a single config/features.neon at boot and merging it into the container:

// app/Providers/NeonConfigServiceProvider.php
public function register(): void
{
    $path = config_path('features.neon');
    if (! is_file($path)) {
        return;
    }
    config(['features' => Neon::decodeFile($path)]);
}

decodeFile() is atomic (reads the whole file before parsing), so a half-written deploy artifact will throw rather than yield a truncated array.

Implementation Patterns

1. A dedicated config loader for a NEON file

Wrap decoding in one place so you control caching, error surface, and file resolution:

namespace App\Support;

use Nette\Neon\Neon;
use Nette\Neon\Exception as NeonException;

final class NeonConfig
{
    /** @return array<string, mixed> */
    public static function load(string $path): array
    {
        try {
            $data = Neon::decodeFile($path);
        } catch (NeonException $e) {
            throw new \RuntimeException("Invalid NEON config: {$path} — {$e->getMessage()}", 0, $e);
        }

        return is_array($data) ? $data : [];
    }
}

Then merge in a provider registered before anything that reads those keys:

public function register(): void
{
    $base = NeonConfig::load(config_path('services.neon'));
    $env  = NeonConfig::load(config_path('services.'.app()->environment().'.neon'));

    config(['services_ext' => array_replace_recursive($base, $env)]);
}

This mirrors Laravel's mergeConfigFrom() semantics but gives you NEON's readable block syntax for deeply nested structures instead of PHP arrays.

2. Environment-specific overrides in one file

NEON's nested block arrays are the selling point. Keep one file per environment rather than scattering env() calls throughout the tree:

# config/services.neon
queue:
    driver: redis
    connection: default
    retry_after: 90

# config/services.production.neon
queue:
    connection: priority
    retry_after: 120

Deep-merge the environment file on top of the base file at boot. This keeps the production delta reviewable in a pull request.

3. CI linting

Syntax errors in a hand-edited NEON file must never reach runtime. Add an artisan command that lints every .neon file in the repo:

use Nette\Neon\Linter;
use Symfony\Component\Finder\Finder;

public function handle(): int
{
    $failed = 0;
    foreach (Finder::create()->files()->in(base_path())->name('*.neon') as $file) {
        try {
            Linter::lint($file->getContents());
        } catch (\Nette\Neon\Exception $e) {
            $this->error("{$file->getRelativePathname()}: {$e->getMessage()}");
            $failed++;
        }
    }

    return $failed === 0 ? self::SUCCESS : self::FAILURE;
}

Wire it into your CI pipeline next to phpstan and pint. Note that linting only proves the file parses — it will not catch semantic drift such as a date-shaped value quietly becoming a string (see Gotchas), so add a separate assertion pass over the decoded array when those keys matter.

4. Encoding for tooling output, not for config you own

Neon::encode($data) is most useful for generating fixtures, scaffolding new config files, or producing diffs that humans review:

file_put_contents(
    base_path('stubs/config.neon'),
    Neon::encode($defaults, blockMode: true)
);

blockMode: true forces the nested-block format; the default is inline flow style, which is compact but unreadable for anything hierarchical. The third parameter controls indentation (default is a tab).

5. AST traversal when you need line numbers or rewrites

The decoder's return value is plain PHP, but Parser gives you an AST with $startLine / $endLine on every node. That is how you build a custom "this config key was deprecated, see line 14" diagnostics pass:

use Nette\Neon\Parser;
use Nette\Neon\Node;
use Nette\Neon\Traverser;

$ast = (new Parser)->parse($neonString);

(new Traverser(function (Node $node) {
    if ($node instanceof Node\StringNode && $node->value === 'legacy_queue') {
        throw new \RuntimeException("Deprecated value on line {$node->startLine}");
    }
}))->traverse($ast);

Use DontTraverseChildren / StopTraversal from Nette\Neon to exit early instead of walking the whole tree.

6. Normalising decoded values at the boundary

Because typed scalars (dates, entities, big integers) can appear in the result tree, funnel every decoded array through a normaliser before it reaches application code:

final class NeonNormalizer
{
    public static function normalize(mixed $value): mixed
    {
        if ($value instanceof \Nette\Neon\Entity) {
            return ['__entity' => $value->value, 'args' => $value->attributes];
        }
        if ($value instanceof \DateTimeInterface) {
            return $value->format(\DateTimeInterface::ATOM);
        }
        if (is_array($value)) {
            return array_map([self::class, 'normalize'], $value);
        }

        return $value;
    }
}

One place to look when a config value "is not what I typed" saves hours of dd() archaeology.

Gotchas and Tips

  • Tabs, not spaces. NEON is indentation-sensitive and the encoder emits tabs by default. Mixed indentation in block strings was a bug fixed only in v3.4.6 — if your team uses spaces, pass indentation: ' ' to Neon::encode() consistently and never mix.
  • decode() does not strip a UTF-8 BOM. Since v3.3.1 BOM removal was dropped. If your editor writes one, strip it yourself (ltrim($s, "\u{FEFF}")) or use decodeFile(). A BOM causes a confusing "Unexpected character" on line 1.
  • Invalid UTF-8 is asymmetric. The decoder throws on malformed UTF-8 (only UTF-8 mode is supported since v3.2.0); the encoder replaces bad sequences with U+FFFD instead of failing. Do not rely on encode() to validate data.
  • Big integers become strings. Since v3.4.3 anything exceeding PHP_INT_MAX decodes to a string, which is safe but surprising if you type-hint int. Cast explicitly at the boundary and expect TypeError otherwise.
  • Dates decode to DateTimeImmutable, not DateTime (since v2.4.0). Values that look like timestamps silently become objects — including strings in your config that happen to match the pattern. Quote them ('2024-01-01') if you want to keep them as strings. Keys are not date-decoded, only values.
  • Invalid date-shaped literals now decode to strings (v3.4.9). 2024-02-30 and 2024-13-01 used to be a parse failure; they now come back as the plain strings '2024-02-30' / '2024-13-01'. This is the dangerous combination with the previous point: a date-shaped value is now either a DateTimeImmutable or a string, and you cannot tell from the source text alone. If your code assumes "date-shaped ⇒ object", a typo'd date in a config file will no longer blow up at boot — it will surface later as a TypeError or, worse, be persisted as a string. Validate date keys explicitly in your loader (instanceof DateTimeInterface check, or quote nothing and assert the type), and add a test for the typo case.
  • !!chain is a reserved entity name since v3.4.9. It is now used internally for chained entities and can no longer appear in input — a file containing !!chain will be rejected at parse time. If you generate NEON from templates or user input, add a guard so a user-supplied key/value never produces that token; Linter::lint() in CI is the cheapest place to catch it. The flip side: chained-entity syntax is now expressible natively, so Parser output can contain entity nodes you did not write by hand.
  • INF and NAN throw on encode (v3.4.1). If you ever pass computed floats into encode(), guard with is_finite() first.
  • on / off were removed in v3.4.0 as boolean literals. Only true, false, null, yes, no remain. Old Nette configs copied into your repo will break with a syntax error.
  • Entity syntax (foo(bar)) returns Nette\Neon\Entity objects, not arrays. If a config value looks like Factory(...), your decoded array contains an object. Either avoid the syntax in user-facing config or handle Entity instances explicitly when normalizing.
  • PHP version floor and ceiling. v3.4.9 adds PHP 8.6 support, so the package is safe to carry through the next PHP minor upgrade in CI. Check that your pinned version is ^3.4.9 before you bump the Docker base image, otherwise you can end up with a PHP 8.6 runtime and a NEON version that has never been tested against it.
  • config:cache is your real risk. Laravel's config cache serializes the config() array — anything you merged from a NEON file is frozen at cache time. The NEON file is then not re-read until the cache is cleared. Either accept that (the normal Laravel contract) or read the NEON file lazily outside the config repository and cache it yourself with Cache::remember() keyed on filemtime().
  • Parse errors point at lines, not keys. Wrap decoding so the exception message includes the file path — the raw Nette\Neon\Exception message alone ("Unexpected ':' on line 7") is useless when you have fifteen .neon files.
  • License is NOASSERTION on Packagist. Verify the actual license terms with your legal/compliance process before shipping it in a distributed product; for internal tooling it is a non-issue.
  • No Laravel-native integration exists. There is no facade, no config driver, no cache integration. Everything above is glue you own — keep it in one small class (NeonConfig) rather than sprinkling Neon::decode() calls across providers, so a future swap to YAML or JSON is a one-file change.
  • Pin the patch version deliberately. Three of the behaviours above (date fallback, !!chain reservation, PHP 8.6 support) landed inside the 3.4 line. A ^3.4 constraint means a routine composer update can change how an existing config file decodes without any code change on your side — lock the version and treat NEON upgrades as a reviewed change with the linter run against your real config tree.
  • Extension point if you outgrow the defaults: subclass nothing — instead Parser → Traverser → your own visitor. The AST is stable since v3.3.0, and Traverser can substitute nodes, which is how you implement !include style directives yourself by expanding StringNode values during traversal.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tobya/saloonfire
kjmtrue/laravel-debugbar
hexincorp/debugbar
razorbacks/laravel-shibboleth
adilazhari/laravel-idempotency
mamitech/laravel-slow-query-log
mamitech/laravel-http-logger
medeirosrafael/docxmustache
webdevlabs/phreak
vis/log-envelope
digitickets/lalit
acacha/names
scool/foundation
frankkessler/guzzle-oauth2-middleware
delatbabel/site-config
delatbabel/applog
eusonlito/laravel-gettext
henzeb/housepedia-intelligence-laravel
aliqsm/laravel-authkit
itsjustvita/laravel-wp-migrator