This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Nette DI is a compiled Dependency Injection Container for PHP - a core component of the Nette Framework. This is a library/framework component, not an application.
Key characteristics:
- Compiled container generates optimized PHP code for maximum performance
- Full autowiring support with type-based dependency resolution
- NEON configuration format for human-friendly service definitions
- Supports PHP 8.1 - 8.5
- ~5,900 lines of production code
Tests use Nette Tester (not PHPUnit) with .phpt file format:
# Run all tests
vendor/bin/tester tests -s -C
# Run specific directory
vendor/bin/tester tests/DI/ -s -C
# Run specific test file
vendor/bin/tester tests/DI/Compiler.configurator.phpt -s -CFlags explained:
-s- show output from tests-C- use system-wide php.ini
# Run PHPStan (level 5)
composer run phpstan# Quick validation
composer run tester
composer run phpstanAll tests use .phpt format with embedded test cases:
<?php
declare(strict_types=1);
use Tester\Assert;
require __DIR__ . '/../bootstrap.php';
test('description of what is tested', function () {
$container = createContainer(new Compiler, 'services: ...neon...');
Assert::type(ServiceClass::class, $container->getByType(ServiceClass::class));
});
testException('description', function () {
// code that should throw
}, ExpectedException::class, 'Expected message');createContainer($source, $config, $params = [])
- Compiles and instantiates container from config
$sourcecan beCompilerorContainerBuilder$configis NEON string or file path- Returns compiled container instance
- Generated code saved to
tests/tmp/{pid}/code.phpfor debugging
getTempDir()
- Returns per-process temporary directory:
tests/tmp/{pid}/ - Automatically cleaned via garbage collection
Notes::add($message) / Notes::fetch()
- Test notification system for debugging
- Add messages during execution, fetch for assertions
Testing service registration:
$container = createContainer(new Compiler, '
services:
- MyService
');Testing with fixtures:
$loader = new Loader;
$config = $loader->load(__DIR__ . '/files/config.neon');
$container = createContainer(new Compiler, $config);Testing generated code:
$builder = new ContainerBuilder;
$builder->addDefinition('foo')->setType(MyClass::class);
$code = (new PhpGenerator($builder))->generate('Container1');
// inspect $codeThe container compilation happens in distinct phases:
- Load - Configuration files loaded and merged (
Config\Loader) - Extensions - Compiler extensions process configuration and register services (
CompilerExtension) - Resolve - Dependencies resolved, types validated (
Resolver) - Generate - PHP code generated for container class (
PhpGenerator) - Runtime - Compiled container instantiated and used (
Container)
Container.php (11KB)
- Runtime container holding service instances
- Provides services via
getService(),getByType(),getByName() - Manages autowiring metadata, tags, aliases
- Lazy loading and circular dependency detection
Compiler.php (8.6KB)
- Orchestrates compilation process
- Manages compiler extensions
- Loads and processes configuration
- Generates container code
ContainerBuilder.php (9.8KB)
- Builds service definition graph during compilation
- Central registry for all service definitions
- Handles autowiring setup
- Validates service configurations
Resolver.php (21KB - largest file)
- Core dependency resolution logic
- Resolves
Reference,Statement, and type references - Detects circular dependencies
- Handles complex autowiring scenarios
PhpGenerator.php (5.6KB)
- Generates optimized PHP code for container
- Uses
nette/php-generatorfor code emission - Creates type-safe service factory methods
- Produces highly optimized, readable code
Multiple definition types for different service patterns:
ServiceDefinition- Standard service with constructor/setupFactoryDefinition- Auto-generated factory from interfaceAccessorDefinition- Service accessor (getter)LocatorDefinition- Dynamic service locatorImportedDefinition- External service referenceReference- Reference to another service (@serviceName)Statement- Callable/function call (trim(...))
NeonAdapter.php - Primary configuration format
- Processes NEON syntax into service definitions
- Handles special syntax:
@serviceName- service references%paramName%- parameter expansiontrim(...)- first-class callable statements!suffix - prevent merging
- Uses visitor pattern with
NodeTraverser
Loader.php - Configuration file loading
- Merges multiple config files
- Environment-specific overrides
- Parameter inheritance
Built-in extensions providing core functionality:
ServicesExtension- Registers services fromservices:sectionParametersExtension- Handles container parametersSearchExtension- Auto-registration by file patternsInjectExtension- Property/method injection (#[Inject])DecoratorExtension- Service decoration patternsDIExtension- DI-specific configurationExtensionsExtension- Extension management
Create custom extensions by extending CompilerExtension.
Debug panel showing:
- All registered services with types
- Service tags and wiring info
- Container parameters
- Compilation time
- Service instantiation status
- Compiled Container Pattern - Container pre-generated as PHP code, not interpreted at runtime
- Builder Pattern -
ContainerBuilderconstructs service graph before code generation - Visitor Pattern - NEON adapter uses traverser with visitors to process configuration tree
- Extension Point Pattern -
CompilerExtensionallows pluggable compilation customization - Lazy Loading - Services instantiated on-demand, not upfront
- Code Generation - Runtime container is optimized PHP code with zero interpretation overhead
The primary configuration format uses special syntax understood by NeonAdapter:
Service references:
services:
logger: FileLogger
mailer:
factory: Mailer
setup:
- setLogger(@logger) # Reference by name
- setConnection(@Nette\Database\Connection) # Reference by typeFirst-class callables (since 3.2.0):
services:
- MyService(trim(...)) # Callable passed as argument
- Factory::create(...) # Factory method callable
- UserService(@user::logout(...)) # Equivalent to [@user, 'logout']Parameters:
parameters:
logFile: /var/log/app.log
mailer:
host: smtp.example.com
user: admin
services:
- FileLogger(%logFile%) # Parameter expansion
- Mailer(%mailer.host%, %mailer.user%) # Nested parameter accessExpression language - create objects and call functions:
services:
- DateTime() # Create object
- Collator::create(%locale%) # Call static method
database: DatabaseFactory::create()
router: @routerFactory::create() # Call method on serviceMethod chaining (use :: instead of ->):
parameters:
currentDate: DateTime()::format('Y-m-d')
# PHP: (new DateTime())->format('Y-m-d')
host: @http.request::getUrl()::getHost()
# PHP: $this->getService('http.request')->getUrl()->getHost()Special functions:
services:
- Foo(
id: int(::getenv('ProjectId')) # Lossless type casting
productionMode: not(%debugMode%) # Boolean negation
bars: typed(Bar) # Array of all Bar services
loggers: tagged(logger) # Array of services with 'logger' tag
)Constants:
services:
- DirectoryIterator(%tempDir%, FilesystemIterator::SKIP_DOTS)
phpVersion: ::constant(PHP_VERSION)Prevent merging with ! suffix:
services:
database!: CustomConnection # Won't be merged with parent config
items!: # Replace array instead of merging
- newItemAutowiring automatically passes services to constructors and methods based on type hints. Understanding its nuances is critical when working with this codebase.
- Exactly one service of each type must exist in the container
- Multiple services of same type cause autowiring to fail with exception
- Services can be excluded from autowiring using
autowired: false
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb:
create: PDO('sqlite::memory:')
autowired: false # Excluded from autowiring
articles: ArticleRepository # Gets mainDb injectedImportant: In Nette, autowired: false means "don't pass this service to others" (different from Symfony where it means "don't autowire constructor args").
When multiple services of same type exist, mark one as preferred:
services:
mainDb:
create: PDO(%dsn%, %user%, %password%)
autowired: PDO # Becomes preferred for PDO type
tempDb:
create: PDO('sqlite::memory:')
articles: ArticleRepository # Gets mainDbLimit which types a service can be autowired for:
services:
parent: ParentClass
child:
create: ChildClass
autowired: ChildClass # Only autowired for ChildClass type, not ParentClass
# Can also use 'self' as alias for current class
parentDep: ParentDependent # Gets parent service
childDep: ChildDependent # Gets child serviceMultiple types can be specified:
autowired: [BarClass, FooInterface]How narrowing works: Service is only autowired when the required type matches or is a subtype of the narrowed type.
Autowiring can pass arrays of services:
class ShipManager
{
/**
* @param Shipper[] $shippers
*/
public function __construct(array $shippers)
{}
}The container automatically passes all Shipper services (excluding those with autowired: false).
Alternative using typed() function:
services:
- ShipManager(typed(Shipper))Autowiring only works for objects and arrays of objects. Scalar values (strings, numbers, booleans) must be specified in configuration or wrapped in a settings object.
Simple class instantiation:
services:
database: PDO('sqlite::memory:')Multi-line with additional configuration:
services:
database:
create: PDO('sqlite::memory:') # or 'factory:' (both work)
setup: ...
tags: ...Static method factories:
services:
database: DatabaseFactory::create()
router: @routerFactory::create() # Call method on another serviceWith explicit type (when return type not declared):
services:
database:
create: DatabaseFactory::create()
type: PDONamed arguments (preferred for clarity):
services:
database: PDO(
username: root
password: secret
dsn: 'mysql:host=127.0.0.1;dbname=test'
)Omit arguments to use defaults or autowiring:
services:
foo: Foo(_, %appDir%) # First arg autowired, second is parameterCall methods after service creation:
services:
database:
create: PDO(%dsn%, %user%, %password%)
setup:
- setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)
- $value = 123 # Set property
- '$onClick[]' = [@bar, clickHandler] # Add to array
- My\Helpers::initializeFoo(@self) # Pass service to static method
- @anotherService::setFoo(@self) # Call method on other serviceEnable globally or per-service:
di:
lazy: true # Global setting
services:
foo:
create: Foo
lazy: false # Override for specific serviceLazy services return proxy objects; actual instantiation happens on first method/property access. Only works for user-defined classes.
Organize and query services by tags:
services:
foo:
create: Foo
tags:
- cached
logger: monolog.logger.event # Tag with valueRetrieve tagged services:
$names = $container->findByTag('logger');
// ['foo' => 'monolog.logger.event', ...]Or in configuration:
services:
- LoggersDependent(tagged(logger))Modify services registered by extensions:
services:
application.application:
create: MyApplication
alteration: true # Indicates we're modifying existing service
setup:
- '$onStartup[]' = [@resource, init]Remove original configuration:
services:
application.application:
alteration: true
reset:
- arguments
- setup
- tagsRemove service entirely:
services:
cache.journal: falseApply setup to all services of a specific type:
decorator:
App\Presentation\BasePresenter:
setup:
- setProjectId(10)
- $absoluteUrls = true
InjectableInterface:
tags: [mytag: 1]
inject: trueUseful for:
- Calling methods on all presenters
- Setting tags on interfaces
- Enabling inject mode for specific types
Automatically register services by file/class patterns:
search:
- in: %appDir%/Forms
files:
- *Factory.php
classes:
- *Factory
- in: %appDir%/Model
extends:
- App\*Form
implements:
- App\*FormInterface
exclude:
files: ...
classes: ...
tags: [autoregistered]Filtering options:
files:- Filter by filename patternclasses:- Filter by class name patternextends:- Select classes extending specified classesimplements:- Select classes implementing interfacesexclude:- Exclusion rules (same keys as above)tags:- Tags to assign to all registered services
Technical container configuration:
di:
debugger: true # Show DIC in Tracy Bar
excluded: [...] # Parameter types never autowired
lazy: false # Enable lazy services globally (PHP 8.4+)
parentClass: ... # Base class for DI container
export:
parameters: false # Don't export parameters to metadata
tags: # Export only specific tags
- event.subscriber
types: # Export only specific types for autowiring
- Nette\Database\ConnectionMetadata optimization: Reduce generated container size by limiting exported metadata to only what's actually used.
includes:
- parameters.php # Can include PHP files returning arrays
- services.neon
- presenters.neonMerging behavior:
- Later files override earlier ones
- Arrays are merged (unless
!suffix used) - File containing
includeshas higher priority than included files
Extensions customize the compilation process by implementing up to 4 methods called sequentially:
Define and validate extension configuration:
class BlogExtension extends Nette\DI\CompilerExtension
{
public function getConfigSchema(): Nette\Schema\Schema
{
return Expect::structure([
'postsPerPage' => Expect::int(),
'allowComments' => Expect::bool()->default(true),
]);
}
}Access config via $this->config (stdClass object).
Register services to container:
public function loadConfiguration()
{
$builder = $this->getContainerBuilder();
$builder->addDefinition($this->prefix('articles'))
->setFactory(App\Model\HomepageArticles::class, ['@connection'])
->addSetup('setLogger', ['@logger']);
}Important: Use $this->prefix('name') to avoid service name conflicts.
Loading from NEON:
public function loadConfiguration()
{
$this->compiler->loadDefinitionsFromConfig(
$this->loadFromFile(__DIR__ . '/blog.neon')['services'],
);
}In NEON file, use @extension to reference current extension's services.
Modify existing services or establish relationships:
public function beforeCompile()
{
$builder = $this->getContainerBuilder();
foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) {
$builder->getDefinition($serviceName)->addSetup('setLogger');
}
}Called after all loadConfiguration() methods complete. Service graph is fully defined.
Modify generated container class:
public function afterCompile(Nette\PhpGenerator\ClassType $class)
{
$method = $class->getMethod('__construct');
// Modify generated PHP code
}Container class already generated as ClassType object. Can modify before writing to cache.
Add code to run after container instantiation:
public function loadConfiguration()
{
// Auto-start session
if ($this->config->session->autoStart) {
$this->initialization->addBody('$this->getService("session")->start()');
}
// Instantiate services tagged with 'run'
foreach ($this->getContainerBuilder()->findByTag('run') as $name => $foo) {
$this->initialization->addBody('$this->getService(?);', [$name]);
}
}Nette DI can generate factory and accessor implementations from interfaces.
Define interface:
interface ArticleFactory
{
function create(): Article;
}Register in config:
services:
- ArticleFactoryNette generates the implementation. Dependencies autowired into Article constructor.
Parameterized factories:
interface ArticleFactory
{
function create(int $authorId): Article;
}
class Article
{
public function __construct(
private Nette\Database\Connection $db,
private int $authorId, // Matched by name from factory method
) {}
}Advanced configuration:
services:
articleFactory:
implement: ArticleFactory
arguments:
authorId: 123 # Fixed value passed to constructor
setup:
- setAuthorId($authorId) # Or via setterProvide lazy-loading for dependencies:
interface PDOAccessor
{
function get(): PDO;
}services:
- PDOAccessor
- PDO(%dsn%, %user%, %password%)Accessor returns same instance on repeated calls. Database connection only created on first get() call.
If multiple services of same type exist, specify which one: - PDOAccessor(@db1)
Combine multiple factories and accessors in one interface:
interface MultiFactory
{
function createArticle(): Article;
function getDb(): PDO;
}Definition with list (3.2.0+):
services:
- MultiFactory(
article: Article
db: PDO(%dsn%, %user%, %password%)
)Or with references:
services:
article: Article
- PDO(%dsn%, %user%, %password%)
- MultiFactory(
article: @article
db: @\PDO
)When adding features to the DI container:
- Determine scope - Does it need new definition type, compiler extension, or core change?
- Update definitions - Add to
ContainerBuilderif new service type - Implement resolution - Update
Resolverif special dependency handling needed - Generate code - Modify
PhpGeneratorto emit correct PHP code - Add tests - Create
.phpttest files demonstrating usage - Update docs - Changes to configuration syntax need documentation
Inspect generated container:
- Generated code is cached in temp directory
- Use
ContainerLoaderwithautoRebuild: trueduring development - Check
tests/tmp/{pid}/code.phpduring test runs
Use Tracy panel:
- Shows all registered services and their state
- Reveals autowiring metadata
- Displays compilation time
Test helpers:
Notes::add()for debug messages in tests- Examine
tests/DI/expected/for expected code output - Use
Tester\FileMock::create()for inline NEON configs
- Strict types required - All files must have
declare(strict_types=1) - NEON syntax sensitivity - Indentation matters, references need
@prefix - Circular dependencies - Resolver detects but requires careful definition ordering
- Generated code cache - Use
autoRebuild: trueto avoid stale container during development - Test isolation - Each test gets unique container class name via counter
Follows Nette Coding Standard (based on PSR-12):
- Strict types declaration in all files
- PascalCase for classes, camelCase for methods/properties
- Type hints for all parameters, properties, return values
- Two empty lines between methods (per Nette convention)
- Exceptions grouped in
exceptions.phpfiles - Natural language exception messages (e.g., "The file does not exist.")
Entry points for understanding:
readme.md- Excellent overview with working examplessrc/DI/Container.php- Runtime behaviorsrc/DI/Compiler.php- Compilation orchestrationsrc/DI/Resolver.php- Dependency resolution logictests/DI/*.phpt- Real usage patterns
Configuration examples:
tests/DI/files/*.neon- Test fixtures showing NEON syntaxtests/DI/expected/*.php- Expected generated container code