joby/smol-context 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

joby/smol-context

Composer 安装命令:

composer require joby/smol-context

包简介

A lightweight DI container for PHP for global, low-config service and object registration. Use it to resolve and inject services, objects, and config values anywhere. You can execute callables with injected dependencies, instantiate objects with dependencies, and even include files using a unique Do

README 文档

README

A lightweight dependency injection container with config integration and docblock-driven file inclusion.

Installation

composer require joby/smol-context

About

smolContext provides a simple, static dependency injection container for PHP. Services are registered and automatically instantiated with their dependencies when retrieved.

Key features:

  • Static API: Access the container globally via Context without passing it around
  • Automatic dependency resolution: Constructor and callable parameters are injected automatically
  • Explicit registration: Services register only under specified class names for predictable behavior
  • Config integration: Inject config values alongside objects using #[ConfigValue]
  • Docblock file inclusion: Include PHP files with variables injected from docblock annotations
  • Context stack: Push/pop container scopes for testing, sub-requests, or rollback workflows

Basic Usage

Registering and Retrieving Services

use Joby\Smol\Context\Context;

// Register a class (lazy-loaded on first get)
Context::register(App\UserService::class);

// Register a concrete instance
Context::register(new App\Logger());

// Get registered services
$users = Context::get(App\UserService::class);
$logger = Context::get(App\Logger::class);

// Services are cached - same instance every time
assert($logger === Context::get(App\Logger::class));

Registration Aliasing

By default, services are registered only under their exact class name. This explicit behavior prevents surprises and makes dependencies clear. However, you can optionally register a service under additional class names using the also parameter.

use Joby\Smol\Context\Context;

// Default: Only retrievable by exact class name
Context::register(App\Services\MysqlDatabase::class);
Context::get(App\Services\MysqlDatabase::class); // ✓ Works
Context::get(App\Contracts\DatabaseInterface::class); // ✗ Throws exception

// Register under a specific interface
Context::register(
    App\Services\MysqlDatabase::class,
    also: App\Contracts\DatabaseInterface::class
);
Context::get(App\Services\MysqlDatabase::class); // ✓ Works
Context::get(App\Contracts\DatabaseInterface::class); // ✓ Works (same instance)

// Register under multiple interfaces/classes
Context::register(
    App\Services\RedisCache::class,
    also: [
        App\Contracts\CacheInterface::class,
        App\Contracts\Storage::class
    ]
);
Context::get(App\Services\RedisCache::class); // ✓ Works
Context::get(App\Contracts\CacheInterface::class); // ✓ Works (same instance)
Context::get(App\Contracts\Storage::class); // ✓ Works (same instance)

// Register under ALL parent classes and interfaces
Context::register(
    App\Services\FileLogger::class,
    also: true
);
// Now retrievable by FileLogger, Logger, LoggerInterface, etc.

When to use also:

  • false (default): For concrete implementations you'll reference directly
  • string: When injecting via a specific interface or parent class (such as replacing something with a child class)
  • array: When the same instance should satisfy multiple contracts
  • true: For widely-used services accessed via various type hints (use sparingly)

Creating Transient Objects

Build objects without caching them in the container:

// Each call creates a new instance
$parser1 = Context::new(App\Parser::class);
$parser2 = Context::new(App\Parser::class);

assert($parser1 !== $parser2);

Checking for Services

if (Context::has(App\UserService::class)) {
    $users = Context::get(App\UserService::class);
}

Executing Callables with Injection

Execute callables with automatic parameter injection:

use Joby\Smol\Context\Context;

Context::register(App\UserService::class);
Context::register(App\Logger::class);

$result = Context::execute(
    function (App\UserService $users, App\Logger $logger): string {
        $logger->log('Processing...');
        return $users->process();
    }
);

Type-hinted object parameters are automatically resolved from the container.

Config Integration

Every container includes a config service (backed by joby/smol-config). Inject config values using the #[ConfigValue] attribute:

use Joby\Smol\Context\Context;
use Joby\Smol\Context\ConfigValue;
use Joby\Smol\Config\Sources\ArraySource;

// Add config source
$runtime = new ArraySource();
$runtime['name'] = 'My Application';
$runtime['host'] = 'localhost';
Context::container()->config->addSource('app', $runtime);
Context::container()->config->addSource('db', $runtime);

// Inject config values into callables
$result = Context::execute(
    function (
        #[ConfigValue('app/name')] string $appName,
        #[ConfigValue('db/host')] string $dbHost,
    ): string {
        return "{$appName} @ {$dbHost}";
    }
);

Mixing Config and Object Injection

use Joby\Smol\Config\Sources\ArraySource;

Context::register(App\Logger::class);

$config = new ArraySource();
$config['debug'] = true;
Context::container()->config->addSource('app', $config);

Context::execute(
    function (
        App\Logger $logger,
        #[ConfigValue('app/debug')] bool $debug,
    ): void {
        if ($debug) {
            $logger->enableDebugMode();
        }
    }
);

Including Files with Docblock Injection

Include PHP files with variables injected from docblock annotations. This is useful for templates, scripts, or configuration files that need access to services.

The Include File

Create a file with dependencies declared in its opening docblock (report.php):

<?php

use App\UserService;
use App\Logger;

/**
 * @var UserService $users
 * @var Logger $logger
 */

$logger->log('Generating report...');
return $users->generateReport();

Including the File

use Joby\Smol\Context\Context;

Context::register(App\UserService::class);
Context::register(App\Logger::class);

$report = Context::include(__DIR__ . '/report.php');

Config Injection in Included Files

Docblocks don't support real PHP attributes, so config injection uses a string that looks like an attribute on the line immediately before @var. This isn't actually an attribute, and you don't even need to formally use the attribute class, it's just so that the syntax is familiar.

<?php

use App\Logger;

/**
 * #[ConfigValue("app/name")]
 * @var string $appName
 *
 * @var Logger $logger
 */

$logger->log("Report for {$appName}");

Type Resolution

Object types can be:

  • Fully qualified: @var \App\UserService $users
  • Imported via use: @var UserService $users
  • Resolved relative to the file's namespace

Context Stack

The context actually maintains an internal stack of containers, allowing temporary scopes for testing, isolated operations, or rollback workflows.

Cloning the Current Container

use Joby\Smol\Context\Context;

Context::register(new App\Logger());
$loggerA = Context::get(App\Logger::class);

// Create isolated scope by cloning
Context::openFromClone();
Context::register(new App\Logger());
$loggerB = Context::get(App\Logger::class);
Context::close();

$loggerC = Context::get(App\Logger::class);

// Back to original scope
assert($loggerA === $loggerC);
assert($loggerA !== $loggerB);

Starting with an Empty Container

Context::openEmpty();
// Fresh container with no services
Context::register(App\TestLogger::class);
// ... test code ...
Context::close();

Using a Custom Container

$container = new Container();
$container->register(App\MockService::class);

Context::openFromContainer($container);
// Use the custom container
Context::close();

Resetting Completely

// Clear stack and current container
Context::reset();

Usage Patterns

Request-Scoped Services

// Register services at application bootstrap
Context::register(App\Database::class);
Context::register(App\Logger::class);

// Use throughout request handling
$router->add(
    new ExactMatcher('users'),
    function (Request $request) {
        $db = Context::get(App\Database::class);
        return Response::json($db->getUsers());
    }
);

Interface-Based Dependency Injection

// Register implementations under their interfaces
Context::register(
    App\Services\PdoDatabase::class,
    also: App\Contracts\DatabaseInterface::class
);

Context::register(
    App\Services\RedisCache::class,
    also: App\Contracts\CacheInterface::class
);

// Route handlers can depend on interfaces
$router->add(
    new ExactMatcher('users'),
    function (
        App\Contracts\DatabaseInterface $db,
        App\Contracts\CacheInterface $cache
    ) {
        // Dependencies injected automatically
        return Response::json($db->getUsers());
    }
);

Background Jobs

// Clone context for job isolation
Context::openFromClone();

try {
    $queue->add(function () {
        $mailer = Context::get(App\Mailer::class);
        $mailer->sendWelcomeEmail();
    });
} finally {
    Context::close();
}

Template Rendering

Create template files that get services injected (templates/email.php):

<?php

use App\Config;

/**
 * #[ConfigValue("app/name")]
 * @var string $appName
 *
 * #[ConfigValue("app/url")]
 * @var string $appUrl
 */
?>
<!DOCTYPE html>
<html>
<head>
    <title><?= htmlspecialchars($appName) ?></title>
</head>
<body>
    <h1>Welcome to <?= htmlspecialchars($appName) ?></h1>
    <p>Visit us at <a href="<?= htmlspecialchars($appUrl) ?>"><?= htmlspecialchars($appUrl) ?></a></p>
</body>
</html>

Render templates with automatic injection:

use Joby\Smol\Config\Sources\ArraySource;

$appConfig = new ArraySource();
$appConfig['name'] = 'My App';
$appConfig['url'] = 'https://example.com';
Context::container()->config->addSource('app', $appConfig);

$html = Context::include(__DIR__ . '/templates/email.php');

API Reference

Static Context Methods

  • Context::register(string|object $classOrObject, string|array|bool $also = false): void - Register a class or instance
  • Context::get(string $class): object - Retrieve a service (cached)
  • Context::new(string $class): object - Create a new instance (not cached)
  • Context::execute(callable $callable): mixed - Execute a callable with dependency injection
  • Context::include(string $file): mixed - Include a PHP file with dependency injection
  • Context::has(string $class): bool - Check if service is registered
  • Context::container(): Container - Access the current container

Stack Operations

  • Context::openFromClone(): void - Clone current container and push it
  • Context::openEmpty(): void - Create empty container and push it
  • Context::openFromContainer(Container $c): void - Use custom container
  • Context::close(): void - Pop stack and restore previous container
  • Context::reset(): void - Clear stack and container

Requirements

Fully tested on PHP 8.3+, static analysis for PHP 8.1+.

License

MIT License - See LICENSE file for details.

joby/smol-context 适用场景与选型建议

joby/smol-context 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 57 次下载、GitHub Stars 达 0, 最近一次更新时间为 2025 年 12 月 15 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 joby/smol-context 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 joby/smol-context 我们能提供哪些服务?
定制开发 / 二次开发

基于 joby/smol-context 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 57
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 30
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-12-15