Skip to content

feat: Add typed Grav container access, enabling IDE autocomplete and static analysis#4050

Open
NoNoNo wants to merge 1 commit intogetgrav:1.8from
NoNoNo:1.8
Open

feat: Add typed Grav container access, enabling IDE autocomplete and static analysis#4050
NoNoNo wants to merge 1 commit intogetgrav:1.8from
NoNoNo:1.8

Conversation

@NoNoNo
Copy link
Copy Markdown

@NoNoNo NoNoNo commented Mar 22, 2026

Summary

This PR enables IDE autocomplete and static analysis for Grav's dependency injection container by adding:

  1. A __get() magic method to Grav\Framework\DI\Container that bridges property access to array access
  2. @property-read PHPDoc annotations on Grav\Common\Grav for all core services

Result: Plugins can now use $this->grav->log->info('Test') instead of $this->grav['log']->info('Test'), with full IDE support.


The Problem

Grav's container uses ArrayAccess with string keys. This provides zero type information to IDEs and static analyzers:

// IDE sees: mixed (no autocomplete, no type checking)
$this->grav['assets']->addInlineCss(...);
$this->grav['config']->get('system.pages.default');
$this->grav['pages']->find($path);

Developers must manually annotate every access:

/** @var Assets $assets */
$assets = $this->grav['assets'];
$assets->addInlineCss(...);  // Now IDE works

This is tedious, error-prone, and inconsistent across the codebase.

Based on analysis of all published Grav plugins, the most accessed services are:

Service Access Count Current IDE Support
twig 673 None
assets 473 None
config 447 None
uri 405 None
page 347 None
language 212 None
locator 178 None
log 135 None
user 130 None
pages 124 None
admin 115 None
debugger 104 None
session 94 None

The Solution

1. __get() Magic Method (Container.php)

Add magic property access that delegates to ArrayAccess::offsetGet():

/**
 * Magic property access — bridges $container->service to $container['service'].
 * Enables IDE autocomplete via @property-read annotations on subclasses.
 */
public function __get(string $name): mixed
{
    return $this->offsetGet($name);
}

public function __isset(string $name): bool
{
    return $this->offsetExists($name);
}

2. @property-read Annotations (Grav.php)

Add PHPDoc annotations for all core services:

/**
 * Grav container is the heart of Grav.
 *
 * Core Services:
 * @property-read \Grav\Common\User\Interfaces\UserCollectionInterface $accounts
 * @property-read \Grav\Common\Assets $assets
 * @property-read \Grav\Common\Backup\Backups $backups
 * @property-read \Grav\Common\Browser $browser
 * @property-read \Grav\Common\Cache $cache
 * @property-read \Grav\Common\Config\Config $config
 * @property-read \Grav\Common\Config\Languages $languages
 * @property-read \Grav\Common\Config\Setup $setup
 * @property-read \Grav\Common\Debugger $debugger
 * @property-read \Grav\Common\Errors\Errors $errors
 * @property-read \Grav\Common\Inflector $inflector
 * @property-read \Grav\Common\Language\Language $language
 * @property-read \Grav\Common\Page\Pages $pages
 * @property-read \Grav\Common\Page\Interfaces\PageInterface $page
 * @property-read \Grav\Common\Scheduler\Scheduler $scheduler
 * @property-read \Grav\Common\Taxonomy $taxonomy
 * @property-read \Grav\Common\Themes $themes
 * @property-read \Grav\Common\Twig\Twig $twig
 * @property-read \Grav\Common\Uri $uri
 * @property-read \Grav\Common\User\User $user
 * @property-read \Grav\Framework\Filesystem\Filesystem $filesystem
 * @property-read \Grav\Framework\Flex\Flex $flex
 * @property-read \Grav\Framework\Session\Messages $messages
 * @property-read \Grav\Framework\Session\Session $session
 * @property-read \Monolog\Logger $log
 * @property-read \RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator $locator
 * @property-read \Symfony\Component\EventDispatcher\EventDispatcher $events
 *
 * Popular Plugin Services:
 * @property-read \Grav\Plugin\Admin\Admin $admin
 */
class Grav extends Container

How It Works

$this->grav->log->info('Test');
       │
       ▼
PHP sees ->log, no $log property
Calls __get('log')
       │
       ▼
__get() calls $this->offsetGet('log')
       │
       ▼
Pimple resolves 'log' closure
Returns Logger instance
       │
       ▼
IDE sees @property-read Logger $log
IDE knows: log is Logger
IDE shows: info(), error(), warning(), etc.

Usage

Before

// Old style — still works
$this->grav['log']->info('Test');
$this->grav['config']->get('system.pages.default');
$this->grav['assets']->addInlineCss('body { color: red; }');

After

// New style — IDE autocomplete works
$this->grav->log->info('Test');
$this->grav->config->get('system.pages.default');
$this->grav->assets->addInlineCss('body { color: red; }');

Both syntaxes work simultaneously. No breaking changes.


Benefits

Aspect Before After
IDE autocomplete None Full
Static analysis Skipped Validated
Type safety None Annotation-based
Refactoring Manual Automated
Code navigation Impossible Instant

Impact

  • Lines changed: ~40 (28 annotations + 2 methods + docblocks)
  • Breaking changes: None
  • Runtime overhead: None (one method call indirection, optimized by OPcache)
  • PHP version: Works with any PHP version Grav supports

Testing

Test Coverage

Two test files with 66 tests covering:

ContainerTest.php (29 tests, 44 assertions):

  • __get() magic method: string, int, array, bool, null, object parameters
  • __get() service resolution: lazy services, factory services, invokable objects
  • __isset() behavior: existing/non-existing, null, false, zero, empty string/array
  • Property vs Array access equivalence
  • Chained property access
  • Edge cases: numeric keys, special characters, unset, extend

GravPropertyAccessTest.php (37 tests, 86 assertions):

  • Core service property access for all annotated services
  • Property access vs Array access equivalence
  • __isset() for all annotated services
  • Chained method calls (config->get, uri->path, etc.)
  • Lazy service resolution
  • Dynamic service registration
  • Exception handling for non-existent services

Running Tests

php -d register_argc_argv=On vendor/bin/codecept run unit tests/unit/Grav/Framework/DI/ContainerTest.php
php -d register_argc_argv=On vendor/bin/codecept run unit tests/unit/Grav/Common/GravPropertyAccessTest.php

Files Changed

File Change
system/src/Grav/Framework/DI/Container.php Added __get() and __isset() methods (21 lines)
system/src/Grav/Common/Grav.php Added @property-read annotations for all services (28 annotations)
tests/unit/Grav/Framework/DI/ContainerTest.php New: 29 tests for Container magic methods
tests/unit/Grav/Common/GravPropertyAccessTest.php New: 37 tests for Grav property access

Migration for Plugins

Plugins can gradually adopt the new syntax:

// Old style — still works, no changes needed
$this->grav['log']->info('Test');

// New style — better IDE support, optional
$this->grav->log->info('Test');

No plugin code changes are required. The old ArrayAccess syntax continues to work.

…()` magic method

This commit enables IDE autocomplete and static analysis for Grav's
dependency injection container, eliminating the need for manual `@var`
annotations when accessing services.

Changes:
- Add `__get()` and `__isset()` magic methods to `Grav\Framework\DI\Container`
    that bridge property access (`$grav->log`) to array access (`$grav[log]`)
- Add `@property-read` PHPDoc annotations to `Grav\Common\Grav` for all
    core services (config, pages, assets, uri, log, twig, etc.) plus
    the popular admin plugin service
- Add comprehensive test suite (66 tests) covering Container magic
    methods and Grav property access

Both syntaxes work simultaneously:
    Old: `$this->grav['log']->info('Test')`  // still works
    New: `$this->grav->log->info('Test')`    // IDE autocomplete works

Benefits:
- Full IDE autocomplete for all container services
- PHPStan/Psalm can validate service method calls
- Instant code navigation to service classes
- No breaking changes, zero runtime overhead
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant